diff --git a/.gitignore b/.gitignore
index 5c6962be151..40e7d2c0395 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,15 +1,15 @@
-.gitignore
-.gradle
-/local.properties
+.gradle/
+local.properties
.DS_Store
-/build
-/captures
-/app/app.iml
-/.idea
-/*.iml
+build/
+captures/
+.idea/
+*.iml
*~
.weblate
*.class
+**/debug/
+**/release/
# vscode / eclipse files
*.classpath
diff --git a/app/.gitignore b/app/.gitignore
deleted file mode 100644
index 53edac5e4f5..00000000000
--- a/app/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.gitignore
-/build
-*.iml
diff --git a/app/build.gradle b/app/build.gradle
index 4680065782e..4d4c2d9d10f 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -105,9 +105,9 @@ ext {
androidxRoomVersion = '2.3.0'
icepickVersion = '3.2.0'
- exoPlayerVersion = '2.12.3'
+ exoPlayerVersion = '2.14.2'
googleAutoServiceVersion = '1.0'
- groupieVersion = '2.9.0'
+ groupieVersion = '2.10.0'
markwonVersion = '4.6.2'
leakCanaryVersion = '2.5'
@@ -189,7 +189,7 @@ dependencies {
// name and the commit hash with the commit hash of the (pushed) commit you want to test
// This works thanks to JitPack: https://jitpack.io/
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
- implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.21.11'
+ implementation 'com.github.TeamNewPipe:NewPipeExtractor:f6f2724634'
/** Checkstyle **/
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
@@ -208,14 +208,17 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata:${androidxLifecycleVersion}"
implementation "androidx.lifecycle:lifecycle-viewmodel:${androidxLifecycleVersion}"
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
- implementation 'androidx.media:media:1.3.1'
+ implementation 'androidx.media:media:1.4.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
- implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.room:room-runtime:${androidxRoomVersion}"
implementation "androidx.room:room-rxjava3:${androidxRoomVersion}"
kapt "androidx.room:room-compiler:${androidxRoomVersion}"
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
+ // Newer version specified to prevent accessibility regressions with RecyclerView, see:
+ // 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.2.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d92d0b5bf82..cc631af7a17 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -256,6 +256,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -325,8 +340,12 @@
+
+
+
+
diff --git a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
index 433c155c258..6394433773c 100644
--- a/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
+++ b/app/src/main/java/androidx/fragment/app/FragmentStatePagerAdapterMenuWorkaround.java
@@ -51,8 +51,12 @@
*
{@link #saveState()}
* {@link #restoreState(Parcelable, ClassLoader)}
*
+ *
+ * @deprecated Switch to {@link androidx.viewpager2.widget.ViewPager2} and use
+ * {@link androidx.viewpager2.adapter.FragmentStateAdapter} instead.
*/
@SuppressWarnings("deprecation")
+@Deprecated
public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapter {
private static final String TAG = "FragmentStatePagerAdapt";
private static final boolean DEBUG = false;
@@ -86,9 +90,10 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt
private final int mBehavior;
private FragmentTransaction mCurTransaction = null;
- private final ArrayList mSavedState = new ArrayList();
- private final ArrayList mFragments = new ArrayList();
+ private final ArrayList mSavedState = new ArrayList<>();
+ private final ArrayList mFragments = new ArrayList<>();
private Fragment mCurrentPrimaryItem = null;
+ private boolean mExecutingFinishUpdate;
/**
* Constructor for {@link FragmentStatePagerAdapterMenuWorkaround}
@@ -208,7 +213,7 @@ public void destroyItem(@NonNull final ViewGroup container, final int position,
mFragments.set(position, null);
mCurTransaction.remove(fragment);
- if (fragment == mCurrentPrimaryItem) {
+ if (fragment.equals(mCurrentPrimaryItem)) {
mCurrentPrimaryItem = null;
}
}
@@ -247,7 +252,19 @@ public void setPrimaryItem(@NonNull final ViewGroup container, final int positio
@Override
public void finishUpdate(@NonNull final ViewGroup container) {
if (mCurTransaction != null) {
- mCurTransaction.commitNowAllowingStateLoss();
+ // We drop any transactions that attempt to be committed
+ // from a re-entrant call to finishUpdate(). We need to
+ // do this as a workaround for Robolectric running measure/layout
+ // calls inline rather than allowing them to be posted
+ // as they would on a real device.
+ if (!mExecutingFinishUpdate) {
+ try {
+ mExecutingFinishUpdate = true;
+ mCurTransaction.commitNowAllowingStateLoss();
+ } finally {
+ mExecutingFinishUpdate = false;
+ }
+ }
mCurTransaction = null;
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/BaseFragment.java b/app/src/main/java/org/schabi/newpipe/BaseFragment.java
index 0be42764838..16ddb83766e 100644
--- a/app/src/main/java/org/schabi/newpipe/BaseFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/BaseFragment.java
@@ -21,7 +21,6 @@ public abstract class BaseFragment extends Fragment {
//These values are used for controlling fragments when they are part of the frontpage
@State
protected boolean useAsFrontPage = false;
- private boolean mIsVisibleToUser = false;
public void useAsFrontPage(final boolean value) {
useAsFrontPage = value;
@@ -85,12 +84,6 @@ public void onDestroy() {
AppWatcher.INSTANCE.getObjectWatcher().watch(this);
}
- @Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- mIsVisibleToUser = isVisibleToUser;
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Init
//////////////////////////////////////////////////////////////////////////*/
@@ -109,8 +102,7 @@ public void setTitle(final String title) {
if (DEBUG) {
Log.d(TAG, "setTitle() called with: title = [" + title + "]");
}
- if ((!useAsFrontPage || mIsVisibleToUser)
- && (activity != null && activity.getSupportActionBar() != null)) {
+ if (!useAsFrontPage && activity != null && activity.getSupportActionBar() != null) {
activity.getSupportActionBar().setDisplayShowTitleEnabled(true);
activity.getSupportActionBar().setTitle(title);
}
diff --git a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
index 9c392be1e0a..9e43394ac3e 100644
--- a/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
+++ b/app/src/main/java/org/schabi/newpipe/CheckForNewAppVersion.java
@@ -7,7 +7,6 @@
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
-import android.net.ConnectivityManager;
import android.net.Uri;
import android.util.Log;
@@ -15,7 +14,6 @@
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
-import androidx.core.content.ContextCompat;
import androidx.core.content.pm.PackageInfoCompat;
import androidx.preference.PreferenceManager;
@@ -48,7 +46,8 @@ public CheckForNewAppVersion() {
private static final boolean DEBUG = MainActivity.DEBUG;
private static final String TAG = CheckForNewAppVersion.class.getSimpleName();
- private static final String GITHUB_APK_SHA1
+ // 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";
@@ -129,44 +128,37 @@ private static void compareAppVersionAndShowNotification(@NonNull final Applicat
final String versionName,
final String apkLocationUrl,
final int versionCode) {
- final int notificationId = 2000;
-
- if (BuildConfig.VERSION_CODE < versionCode) {
- // 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);
+ if (BuildConfig.VERSION_CODE >= versionCode) {
+ return;
+ }
- 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);
+ // 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 NotificationManagerCompat notificationManager
- = NotificationManagerCompat.from(application);
- notificationManager.notify(notificationId, notificationBuilder.build());
- }
- }
+ 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);
- private static boolean isConnected(@NonNull final App app) {
- final ConnectivityManager connectivityManager =
- ContextCompat.getSystemService(app, ConnectivityManager.class);
- return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null
- && connectivityManager.getActiveNetworkInfo().isConnected();
+ final NotificationManagerCompat notificationManager
+ = NotificationManagerCompat.from(application);
+ notificationManager.notify(2000, notificationBuilder.build());
}
- public static boolean isGithubApk(@NonNull final App app) {
- return getCertificateSHA1Fingerprint(app).equals(GITHUB_APK_SHA1);
+ public static boolean isReleaseApk(@NonNull final App app) {
+ return getCertificateSHA1Fingerprint(app).equals(RELEASE_CERT_PUBLIC_KEY_SHA1);
}
private void checkNewVersion() throws IOException, ReCaptchaException {
@@ -175,9 +167,8 @@ private void checkNewVersion() throws IOException, ReCaptchaException {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(app);
final NewVersionManager manager = new NewVersionManager();
- // Check if user has enabled/disabled update checking
- // and if the current apk is a github one or not.
- if (!prefs.getBoolean(app.getString(R.string.update_app_key), true) || !isGithubApk(app)) {
+ // Check if the current apk is a github one or not.
+ if (!isReleaseApk(app)) {
return;
}
@@ -213,6 +204,7 @@ private void handleResponse(@NonNull final Response response,
// Parse the json from the response.
try {
+
final JsonObject githubStableObject = JsonParser.object()
.from(response.responseBody()).getObject("flavors")
.getObject("github").getObject("stable");
diff --git a/app/src/main/java/org/schabi/newpipe/MainActivity.java b/app/src/main/java/org/schabi/newpipe/MainActivity.java
index 2a8872e2683..0a49e00e4ad 100644
--- a/app/src/main/java/org/schabi/newpipe/MainActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/MainActivity.java
@@ -169,13 +169,54 @@ protected void onCreate(final Bundle savedInstanceState) {
@Override
protected void onPostCreate(final Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
- // Start the service 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();
+
+ final App app = App.getApp();
+ 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
+ // and eventually searching for a new version.
+ // The service searching for a new NewPipe version must not be started in background.
+ startNewVersionCheckService();
+ }
}
- private void setupDrawer() throws Exception {
+ private void setupDrawer() throws ExtractionException {
+ addDrawerMenuForCurrentService();
+
+ toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
+ toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
+ toggle.syncState();
+ mainBinding.getRoot().addDrawerListener(toggle);
+ mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
+ private int lastService;
+
+ @Override
+ public void onDrawerOpened(final View drawerView) {
+ lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
+ }
+
+ @Override
+ public void onDrawerClosed(final View drawerView) {
+ if (servicesShown) {
+ toggleServices();
+ }
+ if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
+ ActivityCompat.recreate(MainActivity.this);
+ }
+ }
+ });
+
+ drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
+ setupDrawerHeader();
+ }
+
+ /**
+ * Builds the drawer menu for the current service.
+ *
+ * @throws ExtractionException
+ */
+ private void addDrawerMenuForCurrentService() throws ExtractionException {
//Tabs
final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
final StreamingService service = NewPipe.getService(currentServiceId);
@@ -214,32 +255,6 @@ private void setupDrawer() throws Exception {
drawerLayoutBinding.navigation.getMenu()
.add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
.setIcon(R.drawable.ic_info_outline);
-
- toggle = new ActionBarDrawerToggle(this, mainBinding.getRoot(),
- toolbarLayoutBinding.toolbar, R.string.drawer_open, R.string.drawer_close);
- toggle.syncState();
- mainBinding.getRoot().addDrawerListener(toggle);
- mainBinding.getRoot().addDrawerListener(new DrawerLayout.SimpleDrawerListener() {
- private int lastService;
-
- @Override
- public void onDrawerOpened(final View drawerView) {
- lastService = ServiceHelper.getSelectedServiceId(MainActivity.this);
- }
-
- @Override
- public void onDrawerClosed(final View drawerView) {
- if (servicesShown) {
- toggleServices();
- }
- if (lastService != ServiceHelper.getSelectedServiceId(MainActivity.this)) {
- ActivityCompat.recreate(MainActivity.this);
- }
- }
- });
-
- drawerLayoutBinding.navigation.setNavigationItemSelectedListener(this::drawerItemSelected);
- setupDrawerHeader();
}
private boolean drawerItemSelected(final MenuItem item) {
@@ -347,11 +362,15 @@ private void toggleServices() {
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_tabs_group);
drawerLayoutBinding.navigation.getMenu().removeGroup(R.id.menu_options_about_group);
+ // Show up or down arrow
+ drawerHeaderBinding.drawerArrow.setImageResource(
+ servicesShown ? R.drawable.ic_arrow_drop_up : R.drawable.ic_arrow_drop_down);
+
if (servicesShown) {
showServices();
} else {
try {
- showTabs();
+ addDrawerMenuForCurrentService();
} catch (final Exception e) {
ErrorActivity.reportUiErrorInSnackbar(this, "Showing main page tabs", e);
}
@@ -359,8 +378,6 @@ private void toggleServices() {
}
private void showServices() {
- drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_up);
-
for (final StreamingService s : NewPipe.getServices()) {
final String title = s.getServiceInfo().getName()
+ (ServiceHelper.isBeta(s) ? " (beta)" : "");
@@ -424,48 +441,6 @@ public void onNothingSelected(final AdapterView> parent) {
menuItem.setActionView(spinner);
}
- private void showTabs() throws ExtractionException {
- drawerHeaderBinding.drawerArrow.setImageResource(R.drawable.ic_arrow_drop_down);
-
- //Tabs
- final int currentServiceId = ServiceHelper.getSelectedServiceId(this);
- final StreamingService service = NewPipe.getService(currentServiceId);
-
- int kioskId = 0;
-
- for (final String ks : service.getKioskList().getAvailableKiosks()) {
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, kioskId, ORDER,
- KioskTranslator.getTranslatedKioskName(ks, this))
- .setIcon(KioskTranslator.getKioskIcon(ks, this));
- kioskId++;
- }
-
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_SUBSCRIPTIONS, ORDER, R.string.tab_subscriptions)
- .setIcon(R.drawable.ic_tv);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_FEED, ORDER, R.string.fragment_feed_title)
- .setIcon(R.drawable.ic_rss_feed);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_BOOKMARKS, ORDER, R.string.tab_bookmarks)
- .setIcon(R.drawable.ic_bookmark);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_DOWNLOADS, ORDER, R.string.downloads)
- .setIcon(R.drawable.ic_file_download);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_tabs_group, ITEM_ID_HISTORY, ORDER, R.string.action_history)
- .setIcon(R.drawable.ic_history);
-
- //Settings and About
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_options_about_group, ITEM_ID_SETTINGS, ORDER, R.string.settings)
- .setIcon(R.drawable.ic_settings);
- drawerLayoutBinding.navigation.getMenu()
- .add(R.id.menu_options_about_group, ITEM_ID_ABOUT, ORDER, R.string.tab_about)
- .setIcon(R.drawable.ic_info_outline);
- }
-
@Override
protected void onDestroy() {
super.onDestroy();
diff --git a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
index 9105ff99288..fcad0b6129c 100644
--- a/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
+++ b/app/src/main/java/org/schabi/newpipe/QueueItemMenuUtil.java
@@ -9,8 +9,8 @@
import androidx.fragment.app.FragmentManager;
-import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
-import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
+import org.schabi.newpipe.database.stream.model.StreamEntity;
+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;
@@ -18,6 +18,9 @@
import java.util.Collections;
public final class QueueItemMenuUtil {
+ private QueueItemMenuUtil() {
+ }
+
public static void openPopupMenu(final PlayQueue playQueue,
final PlayQueueItem item,
final View view,
@@ -47,13 +50,22 @@ public static void openPopupMenu(final PlayQueue playQueue,
false);
return true;
case R.id.menu_item_append_playlist:
- final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(
- Collections.singletonList(item)
+ PlaylistDialog.createCorrespondingDialog(
+ context,
+ Collections.singletonList(new StreamEntity(item)),
+ dialog -> dialog.show(
+ fragmentManager,
+ "QueueItemMenuUtil@append_playlist"
+ )
);
- PlaylistAppendDialog.onPlaylistFound(context,
- () -> d.show(fragmentManager, "QueueItemMenuUtil@append_playlist"),
- () -> PlaylistCreationDialog.newInstance(d)
- .show(fragmentManager, "QueueItemMenuUtil@append_playlist"));
+
+ return true;
+ case R.id.menu_item_channel_details:
+ // An intent must be used here.
+ // Opening with FragmentManager transactions is not working,
+ // as PlayQueueActivity doesn't use fragments.
+ NavigationHelper.openChannelFragmentUsingIntent(context, item.getServiceId(),
+ item.getUploaderUrl(), item.getUploader());
return true;
case R.id.menu_item_share:
shareText(context, item.getTitle(), item.getUrl(),
@@ -65,6 +77,4 @@ public static void openPopupMenu(final PlayQueue playQueue,
popupMenu.show();
}
-
- private QueueItemMenuUtil() { }
}
diff --git a/app/src/main/java/org/schabi/newpipe/RouterActivity.java b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
index feb9e029d7b..4e96f3bb6e5 100644
--- a/app/src/main/java/org/schabi/newpipe/RouterActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/RouterActivity.java
@@ -1,5 +1,8 @@
package org.schabi.newpipe;
+import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
+import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
+
import android.annotation.SuppressLint;
import android.app.IntentService;
import android.content.Context;
@@ -30,6 +33,7 @@
import androidx.fragment.app.FragmentManager;
import androidx.preference.PreferenceManager;
+import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
import org.schabi.newpipe.download.DownloadDialog;
@@ -56,6 +60,7 @@
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.extractor.stream.VideoStream;
import org.schabi.newpipe.ktx.ExceptionUtils;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.helper.PlayerHelper;
import org.schabi.newpipe.player.helper.PlayerHolder;
@@ -69,14 +74,15 @@
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PermissionHelper;
-import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.ThemeHelper;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.util.urlfinder.UrlFinder;
import org.schabi.newpipe.views.FocusOverlayView;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import icepick.Icepick;
@@ -89,9 +95,6 @@
import io.reactivex.rxjava3.functions.Consumer;
import io.reactivex.rxjava3.schedulers.Schedulers;
-import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.AUDIO;
-import static org.schabi.newpipe.extractor.StreamingService.ServiceInfo.MediaCapability.VIDEO;
-
/**
* Get the url from the intent and open it in the chosen preferred player.
*/
@@ -107,6 +110,7 @@ public class RouterActivity extends AppCompatActivity {
protected String currentUrl;
private StreamingService currentService;
private boolean selectionIsDownload = false;
+ private boolean selectionIsAddToPlaylist = false;
private AlertDialog alertDialogChoice = null;
@Override
@@ -350,7 +354,7 @@ private void showDialog(final List choices) {
.setNegativeButton(R.string.just_once, dialogButtonsClickListener)
.setPositiveButton(R.string.always, dialogButtonsClickListener)
.setOnDismissListener((dialog) -> {
- if (!selectionIsDownload) {
+ if (!selectionIsDownload && !selectionIsAddToPlaylist) {
finish();
}
})
@@ -446,6 +450,10 @@ private List getChoicesForService(final StreamingService serv
final AdapterChoiceItem backgroundPlayer = new AdapterChoiceItem(
getString(R.string.background_player_key), getString(R.string.background_player),
R.drawable.ic_headset);
+ final AdapterChoiceItem addToPlaylist = new AdapterChoiceItem(
+ getString(R.string.add_to_playlist_key), getString(R.string.add_to_playlist),
+ R.drawable.ic_add);
+
if (linkType == LinkType.STREAM) {
if (isExtVideoEnabled) {
@@ -482,6 +490,10 @@ private List getChoicesForService(final StreamingService serv
getString(R.string.download),
R.drawable.ic_file_download));
+ // Add to playlist is not necessary for CHANNEL and PLAYLIST linkType since those can
+ // not be added to a playlist
+ returnList.add(addToPlaylist);
+
} else {
returnList.add(showInfo);
if (capabilities.contains(VIDEO) && !isExtVideoEnabled) {
@@ -547,6 +559,12 @@ private void handleChoice(final String selectedChoiceKey) {
return;
}
+ if (selectedChoiceKey.equals(getString(R.string.add_to_playlist_key))) {
+ selectionIsAddToPlaylist = true;
+ openAddToPlaylistDialog();
+ return;
+ }
+
// stop and bypass FetcherService if InfoScreen was selected since
// StreamDetailFragment can fetch data itself
if (selectedChoiceKey.equals(getString(R.string.show_info_key))) {
@@ -572,6 +590,41 @@ private void handleChoice(final String selectedChoiceKey) {
finish();
}
+ private void openAddToPlaylistDialog() {
+ // Getting the stream info usually takes a moment
+ // Notifying the user here to ensure that no confusion arises
+ Toast.makeText(
+ getApplicationContext(),
+ getString(R.string.processing_may_take_a_moment),
+ Toast.LENGTH_SHORT)
+ .show();
+
+ disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ info -> PlaylistDialog.createCorrespondingDialog(
+ getThemeWrapperContext(),
+ Collections.singletonList(new StreamEntity(info)),
+ playlistDialog -> {
+ playlistDialog.setOnDismissListener(dialog -> finish());
+
+ playlistDialog.show(
+ this.getSupportFragmentManager(),
+ "addToPlaylistDialog"
+ );
+ }
+ ),
+ throwable -> handleError(this, new ErrorInfo(
+ throwable,
+ UserAction.REQUESTED_STREAM,
+ "Tried to add " + currentUrl + " to a playlist",
+ currentService.getServiceId())
+ )
+ )
+ );
+ }
+
@SuppressLint("CheckResult")
private void openDownloadDialog() {
disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true)
diff --git a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
index 689f1ead67d..72692a9f591 100644
--- a/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
+++ b/app/src/main/java/org/schabi/newpipe/database/feed/dao/FeedDAO.kt
@@ -7,6 +7,7 @@ import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.reactivex.rxjava3.core.Flowable
+import io.reactivex.rxjava3.core.Maybe
import org.schabi.newpipe.database.feed.model.FeedEntity
import org.schabi.newpipe.database.feed.model.FeedLastUpdatedEntity
import org.schabi.newpipe.database.stream.StreamWithState
@@ -37,7 +38,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getAllStreams(): Flowable>
+ abstract fun getAllStreams(): Maybe>
@Query(
"""
@@ -62,7 +63,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getAllStreamsForGroup(groupId: Long): Flowable>
+ abstract fun getAllStreamsForGroup(groupId: Long): Maybe>
/**
* @see StreamStateEntity.isFinished()
@@ -97,7 +98,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getLiveOrNotPlayedStreams(): Flowable>
+ abstract fun getLiveOrNotPlayedStreams(): Maybe>
/**
* @see StreamStateEntity.isFinished()
@@ -137,7 +138,7 @@ abstract class FeedDAO {
LIMIT 500
"""
)
- abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Flowable>
+ abstract fun getLiveOrNotPlayedStreamsForGroup(groupId: Long): Maybe>
@Query(
"""
diff --git a/app/src/main/java/org/schabi/newpipe/error/EnsureExceptionSerializable.java b/app/src/main/java/org/schabi/newpipe/error/EnsureExceptionSerializable.java
new file mode 100644
index 00000000000..db94de5e5c3
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/error/EnsureExceptionSerializable.java
@@ -0,0 +1,103 @@
+package org.schabi.newpipe.error;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Ensures that a Exception is serializable.
+ * This is
+ */
+public final class EnsureExceptionSerializable {
+ private static final String TAG = "EnsureExSerializable";
+
+ private EnsureExceptionSerializable() {
+ // No instance
+ }
+
+ /**
+ * Ensures that an exception is serializable.
+ *
+ * If that is not the case a {@link WorkaroundNotSerializableException} is created.
+ *
+ * @param exception
+ * @return if an exception is not serializable a new {@link WorkaroundNotSerializableException}
+ * otherwise the exception from the parameter
+ */
+ public static Exception ensureSerializable(@NonNull final Exception exception) {
+ return checkIfSerializable(exception)
+ ? exception
+ : WorkaroundNotSerializableException.create(exception);
+ }
+
+ public static boolean checkIfSerializable(@NonNull final Exception exception) {
+ try {
+ // Check by creating a new ObjectOutputStream which does the serialization
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ ObjectOutputStream oos = new ObjectOutputStream(bos)
+ ) {
+ oos.writeObject(exception);
+ oos.flush();
+
+ bos.toByteArray();
+ }
+
+ return true;
+ } catch (final IOException ex) {
+ Log.d(TAG, "Exception is not serializable", ex);
+ return false;
+ }
+ }
+
+ public static class WorkaroundNotSerializableException extends Exception {
+ protected WorkaroundNotSerializableException(
+ final Throwable notSerializableException,
+ final Throwable cause) {
+ super(notSerializableException.toString(), cause);
+ setStackTrace(notSerializableException.getStackTrace());
+ }
+
+ protected WorkaroundNotSerializableException(final Throwable notSerializableException) {
+ super(notSerializableException.toString());
+ setStackTrace(notSerializableException.getStackTrace());
+ }
+
+ public static WorkaroundNotSerializableException create(
+ @NonNull final Exception notSerializableException
+ ) {
+ // Build a list of the exception + all causes
+ final List throwableList = new ArrayList<>();
+
+ int pos = 0;
+ Throwable throwableToProcess = notSerializableException;
+
+ while (throwableToProcess != null) {
+ throwableList.add(throwableToProcess);
+
+ pos++;
+ throwableToProcess = throwableToProcess.getCause();
+ }
+
+ // Reverse list so that it starts with the last one
+ Collections.reverse(throwableList);
+
+ // Build exception stack
+ WorkaroundNotSerializableException cause = null;
+ for (final Throwable t : throwableList) {
+ cause = cause == null
+ ? new WorkaroundNotSerializableException(t)
+ : new WorkaroundNotSerializableException(t, cause);
+ }
+
+ return cause;
+ }
+
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
index c0d88c8ec83..db3a92d4f93 100644
--- a/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/error/ErrorActivity.java
@@ -77,6 +77,16 @@ public class ErrorActivity extends AppCompatActivity {
private ActivityErrorBinding activityErrorBinding;
+ /**
+ * Reports a new error by starting a new activity.
+ *
+ * Ensure that the data within errorInfo is serializable otherwise
+ * an exception will be thrown!
+ * {@link EnsureExceptionSerializable} might help.
+ *
+ * @param context
+ * @param errorInfo
+ */
public static void reportError(final Context context, final ErrorInfo errorInfo) {
final Intent intent = new Intent(context, ErrorActivity.class);
intent.putExtra(ERROR_INFO, errorInfo);
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
index 0cccfa4fe0e..fe4eef37ac3 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/BlankFragment.java
@@ -20,8 +20,8 @@ public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGrou
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
+ public void onResume() {
+ super.onResume();
setTitle("NewPipe");
// leave this inline. Will make it harder for copy cats.
// If you are a Copy cat FUCK YOU.
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 437cb2fdb2e..8c6e0153776 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
@@ -52,6 +52,7 @@
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
+import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.FragmentVideoDetailBinding;
import org.schabi.newpipe.download.DownloadDialog;
import org.schabi.newpipe.error.ErrorActivity;
@@ -73,8 +74,7 @@
import org.schabi.newpipe.fragments.list.comments.CommentsFragment;
import org.schabi.newpipe.fragments.list.videos.RelatedItemsFragment;
import org.schabi.newpipe.ktx.AnimationType;
-import org.schabi.newpipe.local.dialog.PlaylistAppendDialog;
-import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
+import org.schabi.newpipe.local.dialog.PlaylistDialog;
import org.schabi.newpipe.local.history.HistoryRecordManager;
import org.schabi.newpipe.player.MainPlayer;
import org.schabi.newpipe.player.MainPlayer.PlayerType;
@@ -99,6 +99,7 @@
import org.schabi.newpipe.util.external_communication.ShareUtils;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@@ -444,12 +445,11 @@ public void onClick(final View v) {
break;
case R.id.detail_controls_playlist_append:
if (getFM() != null && currentInfo != null) {
-
- final PlaylistAppendDialog d = PlaylistAppendDialog.fromStreamInfo(currentInfo);
disposables.add(
- PlaylistAppendDialog.onPlaylistFound(getContext(),
- () -> d.show(getFM(), TAG),
- () -> PlaylistCreationDialog.newInstance(d).show(getFM(), TAG)
+ PlaylistDialog.createCorrespondingDialog(
+ getContext(),
+ Collections.singletonList(new StreamEntity(currentInfo)),
+ dialog -> dialog.show(getFM(), TAG)
)
);
}
@@ -594,6 +594,11 @@ private void toggleTitleAndSecondaryControls() {
// Init
//////////////////////////////////////////////////////////////////////////*/
+ @Override
+ public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
+ super.onViewCreated(rootView, savedInstanceState);
+ }
+
@Override // called from onViewCreated in {@link BaseFragment#onViewCreated}
protected void initViews(final View rootView, final Bundle savedInstanceState) {
super.initViews(rootView, savedInstanceState);
@@ -604,6 +609,18 @@ protected void initViews(final View rootView, final Bundle savedInstanceState) {
binding.detailThumbnailRootLayout.requestFocus();
+ binding.detailControlsPlayWithKodi.setVisibility(
+ KoreUtils.shouldShowPlayWithKodi(requireContext(), serviceId)
+ ? View.VISIBLE
+ : View.GONE
+ );
+ binding.detailControlsCrashThePlayer.setVisibility(
+ DEBUG && PreferenceManager.getDefaultSharedPreferences(getContext())
+ .getBoolean(getString(R.string.show_crash_the_player_key), false)
+ ? View.VISIBLE
+ : View.GONE
+ );
+
if (DeviceUtils.isTv(getContext())) {
// remove ripple effects from detail controls
final int transparent = ContextCompat.getColor(requireContext(),
@@ -638,8 +655,14 @@ protected void initListeners() {
binding.detailControlsShare.setOnClickListener(this);
binding.detailControlsOpenInBrowser.setOnClickListener(this);
binding.detailControlsPlayWithKodi.setOnClickListener(this);
- binding.detailControlsPlayWithKodi.setVisibility(KoreUtils.shouldShowPlayWithKodi(
- requireContext(), serviceId) ? View.VISIBLE : View.GONE);
+ if (DEBUG) {
+ binding.detailControlsCrashThePlayer.setOnClickListener(
+ v -> VideoDetailPlayerCrasher.onCrashThePlayer(
+ this.getContext(),
+ this.player,
+ getLayoutInflater())
+ );
+ }
binding.overlayThumbnail.setOnClickListener(this);
binding.overlayThumbnail.setOnLongClickListener(this);
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java
new file mode 100644
index 00000000000..9309a8a4976
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailPlayerCrasher.java
@@ -0,0 +1,159 @@
+package org.schabi.newpipe.fragments.detail;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+
+import com.google.android.exoplayer2.C;
+import com.google.android.exoplayer2.ExoPlaybackException;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.databinding.ListRadioIconItemBinding;
+import org.schabi.newpipe.databinding.SingleChoiceDialogViewBinding;
+import org.schabi.newpipe.player.Player;
+import org.schabi.newpipe.util.ThemeHelper;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+/**
+ * Outsourced logic for crashing the player in the {@link VideoDetailFragment}.
+ */
+public final class VideoDetailPlayerCrasher {
+
+ // This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
+ // or it fails with an IllegalArgumentException
+ // https://stackoverflow.com/a/54744028
+ private static final String TAG = "VideoDetPlayerCrasher";
+
+ private static final Map> AVAILABLE_EXCEPTION_TYPES =
+ getExceptionTypes();
+
+ private VideoDetailPlayerCrasher() {
+ // No impls
+ }
+
+ private static Map> getExceptionTypes() {
+ final String defaultMsg = "Dummy";
+ final Map> exceptionTypes = new LinkedHashMap<>();
+ exceptionTypes.put(
+ "Source",
+ () -> ExoPlaybackException.createForSource(
+ new IOException(defaultMsg)
+ )
+ );
+ exceptionTypes.put(
+ "Renderer",
+ () -> ExoPlaybackException.createForRenderer(
+ new Exception(defaultMsg),
+ "Dummy renderer",
+ 0,
+ null,
+ C.FORMAT_HANDLED
+ )
+ );
+ exceptionTypes.put(
+ "Unexpected",
+ () -> ExoPlaybackException.createForUnexpected(
+ new RuntimeException(defaultMsg)
+ )
+ );
+ exceptionTypes.put(
+ "Remote",
+ () -> ExoPlaybackException.createForRemote(defaultMsg)
+ );
+
+ return Collections.unmodifiableMap(exceptionTypes);
+ }
+
+ private static Context getThemeWrapperContext(final Context context) {
+ return new ContextThemeWrapper(
+ context,
+ ThemeHelper.isLightThemeSelected(context)
+ ? R.style.LightTheme
+ : R.style.DarkTheme);
+ }
+
+ public static void onCrashThePlayer(
+ @NonNull final Context context,
+ @Nullable final Player player,
+ @NonNull final LayoutInflater layoutInflater
+ ) {
+ if (player == null) {
+ Log.d(TAG, "Player is not available");
+ Toast.makeText(context, "Player is not available", Toast.LENGTH_SHORT)
+ .show();
+
+ return;
+ }
+
+ // -- Build the dialog/UI --
+
+ final Context themeWrapperContext = getThemeWrapperContext(context);
+
+ final LayoutInflater inflater = LayoutInflater.from(themeWrapperContext);
+ final RadioGroup radioGroup = SingleChoiceDialogViewBinding.inflate(layoutInflater)
+ .list;
+
+ final AlertDialog alertDialog = new AlertDialog.Builder(getThemeWrapperContext(context))
+ .setTitle("Choose an exception")
+ .setView(radioGroup)
+ .setCancelable(true)
+ .setNegativeButton(R.string.cancel, null)
+ .create();
+
+ for (final Map.Entry> entry
+ : AVAILABLE_EXCEPTION_TYPES.entrySet()) {
+ final RadioButton radioButton = ListRadioIconItemBinding.inflate(inflater).getRoot();
+ radioButton.setText(entry.getKey());
+ radioButton.setChecked(false);
+ radioButton.setLayoutParams(
+ new RadioGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ );
+ radioButton.setOnClickListener(v -> {
+ tryCrashPlayerWith(player, entry.getValue().get());
+ if (alertDialog != null) {
+ alertDialog.cancel();
+ }
+ });
+ radioGroup.addView(radioButton);
+ }
+
+ alertDialog.show();
+ }
+
+ /**
+ * Note that this method does not crash the underlying exoplayer directly (it's not possible).
+ * It simply supplies a Exception to {@link Player#onPlayerError(ExoPlaybackException)}.
+ * @param player
+ * @param exception
+ */
+ private static void tryCrashPlayerWith(
+ @NonNull final Player player,
+ @NonNull final ExoPlaybackException exception
+ ) {
+ Log.d(TAG, "Crashing the player using player.onPlayerError(ex)");
+ try {
+ player.onPlayerError(exception);
+ } catch (final Exception exPlayer) {
+ Log.e(TAG,
+ "Run into an exception while crashing the player:",
+ exPlayer);
+ }
+ }
+}
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 c30b6fc05c9..b9065c9694d 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
@@ -143,7 +143,7 @@ private int getFocusedPosition() {
final View focusedItem = itemsList.getFocusedChild();
final RecyclerView.ViewHolder itemHolder =
itemsList.findContainingViewHolder(focusedItem);
- return itemHolder.getAdapterPosition();
+ return itemHolder.getBindingAdapterPosition();
} catch (final NullPointerException e) {
return -1;
}
@@ -378,6 +378,13 @@ protected void showStreamDialog(final StreamInfoItem item) {
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);
}
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 1d16559acbf..30e38a966a6 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
@@ -98,11 +98,9 @@ public ChannelFragment() {
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (activity != null
- && useAsFrontPage
- && isVisibleToUser) {
+ public void onResume() {
+ super.onResume();
+ if (activity != null && useAsFrontPage) {
setTitle(currentInfo != null ? currentInfo.getName() : name);
}
}
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 f37f487bf52..c25f18e8b72 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
@@ -99,9 +99,12 @@ public void onCreate(final Bundle savedInstanceState) {
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (useAsFrontPage && isVisibleToUser && activity != null) {
+ public void onResume() {
+ super.onResume();
+ if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
+ reloadContent();
+ }
+ if (useAsFrontPage && activity != null) {
try {
setTitle(kioskTranslatedName);
} catch (final Exception e) {
@@ -117,15 +120,6 @@ public View onCreateView(@NonNull final LayoutInflater inflater,
return inflater.inflate(R.layout.fragment_kiosk, container, false);
}
- @Override
- public void onResume() {
- super.onResume();
-
- if (!Localization.getPreferredContentCountry(requireContext()).equals(contentCountry)) {
- reloadContent();
- }
- }
-
/*//////////////////////////////////////////////////////////////////////////
// Menu
//////////////////////////////////////////////////////////////////////////*/
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 f3aa2e30610..a8763af7305 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
@@ -176,6 +176,12 @@ protected void showStreamDialog(final StreamInfoItem item) {
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);
}
diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
index 7de2123832f..d4d73f74ff2 100644
--- a/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/SearchFragment.java
@@ -1088,7 +1088,7 @@ public void handleError() {
//////////////////////////////////////////////////////////////////////////*/
public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder viewHolder) {
- final int position = viewHolder.getAdapterPosition();
+ final int position = viewHolder.getBindingAdapterPosition();
if (position == RecyclerView.NO_POSITION) {
return 0;
}
@@ -1099,7 +1099,7 @@ public int getSuggestionMovementFlags(@NonNull final RecyclerView.ViewHolder vie
}
public void onSuggestionItemSwiped(@NonNull final RecyclerView.ViewHolder viewHolder) {
- final int position = viewHolder.getAdapterPosition();
+ final int position = viewHolder.getBindingAdapterPosition();
final String query = suggestionListAdapter.getItem(position).query;
final Disposable onDelete = historyRecordManager.deleteSearchHistory(query)
.observeOn(AndroidSchedulers.mainThread())
diff --git a/app/src/main/java/org/schabi/newpipe/ktx/View.kt b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
index 8f2249493ce..a1a96b20dbf 100644
--- a/app/src/main/java/org/schabi/newpipe/ktx/View.kt
+++ b/app/src/main/java/org/schabi/newpipe/ktx/View.kt
@@ -299,18 +299,36 @@ private fun View.animateLightSlideAndAlpha(enterOrExit: Boolean, duration: Long,
}
}
-fun View.slideUp(duration: Long, delay: Long, @FloatRange(from = 0.0, to = 1.0) translationPercent: Float) {
+fun View.slideUp(
+ duration: Long,
+ delay: Long,
+ @FloatRange(from = 0.0, to = 1.0) translationPercent: Float
+) {
+ slideUp(duration, delay, translationPercent, null)
+}
+
+fun View.slideUp(
+ duration: Long,
+ delay: Long = 0L,
+ @FloatRange(from = 0.0, to = 1.0) translationPercent: Float = 1.0F,
+ execOnEnd: Runnable? = null
+) {
val newTranslationY = (resources.displayMetrics.heightPixels * translationPercent).toInt()
animate().setListener(null).cancel()
alpha = 0f
translationY = newTranslationY.toFloat()
- visibility = View.VISIBLE
+ isVisible = true
animate()
.alpha(1f)
.translationY(0f)
.setStartDelay(delay)
.setDuration(duration)
.setInterpolator(FastOutSlowInInterpolator())
+ .setListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ execOnEnd?.run()
+ }
+ })
.start()
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
index 794e5a33a25..f272a8831f4 100644
--- a/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/bookmark/BookmarkFragment.java
@@ -78,9 +78,9 @@ public View onCreateView(@NonNull final LayoutInflater inflater,
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (activity != null && isVisibleToUser) {
+ public void onResume() {
+ super.onResume();
+ if (activity != null) {
setTitle(activity.getString(R.string.tab_bookmarks));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
index 93e1141c7ff..2db7e01539f 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java
@@ -1,6 +1,5 @@
package org.schabi.newpipe.local.dialog;
-import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -17,20 +16,14 @@
import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity;
-import org.schabi.newpipe.extractor.stream.StreamInfo;
-import org.schabi.newpipe.extractor.stream.StreamInfoItem;
import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
-import org.schabi.newpipe.player.playqueue.PlayQueueItem;
import org.schabi.newpipe.util.OnClickGesture;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
-import io.reactivex.rxjava3.disposables.Disposable;
public final class PlaylistAppendDialog extends PlaylistDialog {
private static final String TAG = PlaylistAppendDialog.class.getCanonicalName();
@@ -40,47 +33,8 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
- public static Disposable onPlaylistFound(
- final Context context, final Runnable onSuccess, final Runnable onFailed
- ) {
- final LocalPlaylistManager playlistManager =
- new LocalPlaylistManager(NewPipeDatabase.getInstance(context));
-
- return playlistManager.hasPlaylists()
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe(hasPlaylists -> {
- if (hasPlaylists) {
- onSuccess.run();
- } else {
- onFailed.run();
- }
- });
- }
-
- public static PlaylistAppendDialog fromStreamInfo(final StreamInfo info) {
- final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
- dialog.setInfo(Collections.singletonList(new StreamEntity(info)));
- return dialog;
- }
-
- public static PlaylistAppendDialog fromStreamInfoItems(final List items) {
- final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
- final List entities = new ArrayList<>(items.size());
- for (final StreamInfoItem item : items) {
- entities.add(new StreamEntity(item));
- }
- dialog.setInfo(entities);
- return dialog;
- }
-
- public static PlaylistAppendDialog fromPlayQueueItems(final List items) {
- final PlaylistAppendDialog dialog = new PlaylistAppendDialog();
- final List entities = new ArrayList<>(items.size());
- for (final PlayQueueItem item : items) {
- entities.add(new StreamEntity(item));
- }
- dialog.setInfo(entities);
- return dialog;
+ public PlaylistAppendDialog(final List streamEntities) {
+ super(streamEntities);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -104,11 +58,15 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
playlistAdapter.setSelectedListener(new OnClickGesture() {
@Override
public void selected(final LocalItem selectedItem) {
- if (!(selectedItem instanceof PlaylistMetadataEntry) || getStreams() == null) {
+ if (!(selectedItem instanceof PlaylistMetadataEntry)
+ || getStreamEntities() == null) {
return;
}
- onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem,
- getStreams());
+ onPlaylistSelected(
+ playlistManager,
+ (PlaylistMetadataEntry) selectedItem,
+ getStreamEntities()
+ );
}
});
@@ -146,11 +104,17 @@ public void onDestroyView() {
//////////////////////////////////////////////////////////////////////////*/
public void openCreatePlaylistDialog() {
- if (getStreams() == null || !isAdded()) {
+ if (getStreamEntities() == null || !isAdded()) {
return;
}
- PlaylistCreationDialog.newInstance(getStreams()).show(getParentFragmentManager(), TAG);
+ final PlaylistCreationDialog playlistCreationDialog =
+ new PlaylistCreationDialog(getStreamEntities());
+ // Move the dismissListener to the new dialog.
+ playlistCreationDialog.setOnDismissListener(this.getOnDismissListener());
+ this.setOnDismissListener(null);
+
+ playlistCreationDialog.show(getParentFragmentManager(), TAG);
requireDialog().dismiss();
}
@@ -165,7 +129,7 @@ private void onPlaylistsReceived(@NonNull final List play
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist,
@NonNull final List streams) {
- if (getStreams() == null) {
+ if (getStreamEntities() == null) {
return;
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
index f48c72d04f7..6664144cdab 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistCreationDialog.java
@@ -7,29 +7,22 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AlertDialog.Builder;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.databinding.DialogEditTextBinding;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
+import org.schabi.newpipe.util.ThemeHelper;
import java.util.List;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
public final class PlaylistCreationDialog extends PlaylistDialog {
- public static PlaylistCreationDialog newInstance(final List streams) {
- final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
- dialog.setInfo(streams);
- return dialog;
- }
-
- public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appendDialog) {
- final PlaylistCreationDialog dialog = new PlaylistCreationDialog();
- dialog.setInfo(appendDialog.getStreams());
- return dialog;
+ public PlaylistCreationDialog(final List streamEntities) {
+ super(streamEntities);
}
/*//////////////////////////////////////////////////////////////////////////
@@ -39,16 +32,18 @@ public static PlaylistCreationDialog newInstance(final PlaylistAppendDialog appe
@NonNull
@Override
public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
- if (getStreams() == null) {
+ if (getStreamEntities() == null) {
return super.onCreateDialog(savedInstanceState);
}
final DialogEditTextBinding dialogBinding
= DialogEditTextBinding.inflate(getLayoutInflater());
+ dialogBinding.getRoot().getContext().setTheme(ThemeHelper.getDialogTheme(requireContext()));
dialogBinding.dialogEditText.setHint(R.string.name);
dialogBinding.dialogEditText.setInputType(InputType.TYPE_CLASS_TEXT);
- final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireContext())
+ final Builder dialogBuilder = new Builder(requireContext(),
+ ThemeHelper.getDialogTheme(requireContext()))
.setTitle(R.string.create_playlist)
.setView(dialogBinding.getRoot())
.setCancelable(true)
@@ -61,11 +56,10 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
R.string.playlist_creation_success,
Toast.LENGTH_SHORT);
- playlistManager.createPlaylist(name, getStreams())
+ playlistManager.createPlaylist(name, getStreamEntities())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(longs -> successToast.show());
});
-
return dialogBuilder.create();
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
index 2c2987e95ba..c2d4474f897 100644
--- a/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
+++ b/app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistDialog.java
@@ -1,6 +1,8 @@
package org.schabi.newpipe.local.dialog;
import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Window;
@@ -8,23 +10,29 @@
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
+import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.database.stream.model.StreamEntity;
+import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
import org.schabi.newpipe.util.StateSaver;
import java.util.List;
import java.util.Queue;
+import java.util.function.Consumer;
+
+import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
+import io.reactivex.rxjava3.disposables.Disposable;
public abstract class PlaylistDialog extends DialogFragment implements StateSaver.WriteRead {
+
+ @Nullable
+ private DialogInterface.OnDismissListener onDismissListener = null;
+
private List streamEntities;
private org.schabi.newpipe.util.SavedState savedState;
- protected void setInfo(final List entities) {
- this.streamEntities = entities;
- }
-
- protected List getStreams() {
- return streamEntities;
+ public PlaylistDialog(final List streamEntities) {
+ this.streamEntities = streamEntities;
}
/*//////////////////////////////////////////////////////////////////////////
@@ -43,6 +51,10 @@ public void onDestroy() {
StateSaver.onDestroy(savedState);
}
+ public List getStreamEntities() {
+ return streamEntities;
+ }
+
@NonNull
@Override
public Dialog onCreateDialog(final Bundle savedInstanceState) {
@@ -55,6 +67,14 @@ public Dialog onCreateDialog(final Bundle savedInstanceState) {
return dialog;
}
+ @Override
+ public void onDismiss(@NonNull final DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (onDismissListener != null) {
+ onDismissListener.onDismiss(dialog);
+ }
+ }
+
/*//////////////////////////////////////////////////////////////////////////
// State Saving
//////////////////////////////////////////////////////////////////////////*/
@@ -84,4 +104,47 @@ public void onSaveInstanceState(final Bundle outState) {
savedState, outState, this);
}
}
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Getter + Setter
+ //////////////////////////////////////////////////////////////////////////*/
+
+ @Nullable
+ public DialogInterface.OnDismissListener getOnDismissListener() {
+ return onDismissListener;
+ }
+
+ public void setOnDismissListener(
+ @Nullable final DialogInterface.OnDismissListener onDismissListener
+ ) {
+ this.onDismissListener = onDismissListener;
+ }
+
+ /*//////////////////////////////////////////////////////////////////////////
+ // Dialog creation
+ //////////////////////////////////////////////////////////////////////////*/
+
+ /**
+ * Creates a {@link PlaylistAppendDialog} when playlists exists,
+ * otherwise a {@link PlaylistCreationDialog}.
+ *
+ * @param context context used for accessing the database
+ * @param streamEntities used for crating the dialog
+ * @param onExec execution that should occur after a dialog got created, e.g. showing it
+ * @return Disposable
+ */
+ public static Disposable createCorrespondingDialog(
+ final Context context,
+ final List streamEntities,
+ final Consumer onExec
+ ) {
+ return new LocalPlaylistManager(NewPipeDatabase.getInstance(context))
+ .hasPlaylists()
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(hasPlaylists ->
+ onExec.accept(hasPlaylists
+ ? new PlaylistAppendDialog(streamEntities)
+ : new PlaylistCreationDialog(streamEntities))
+ );
+ }
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
index ff7c2848e6a..e28f2d31ad9 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedDatabaseManager.kt
@@ -42,7 +42,7 @@ class FeedDatabaseManager(context: Context) {
fun getStreams(
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
getPlayedStreams: Boolean = true
- ): Flowable> {
+ ): Maybe> {
return when (groupId) {
FeedGroupEntity.GROUP_ALL_ID -> {
if (getPlayedStreams) feedTable.getAllStreams()
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index 965075bf302..5b593bcd7a6 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -21,16 +21,23 @@ package org.schabi.newpipe.local.feed
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.os.Parcelable
+import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
+import android.widget.Button
+import androidx.annotation.AttrRes
import androidx.annotation.Nullable
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
@@ -40,8 +47,10 @@ import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Item
+import com.xwray.groupie.OnAsyncUpdateListener
import com.xwray.groupie.OnItemClickListener
import com.xwray.groupie.OnItemLongClickListener
import icepick.State
@@ -65,10 +74,12 @@ import org.schabi.newpipe.fragments.BaseStateFragment
import org.schabi.newpipe.info_list.InfoItemDialog
import org.schabi.newpipe.ktx.animate
import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
+import org.schabi.newpipe.ktx.slideUp
import org.schabi.newpipe.local.feed.item.StreamItem
import org.schabi.newpipe.local.feed.service.FeedLoadService
import org.schabi.newpipe.local.subscription.SubscriptionManager
import org.schabi.newpipe.player.helper.PlayerHolder
+import org.schabi.newpipe.util.DeviceUtils
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import org.schabi.newpipe.util.StreamDialogEntry
@@ -76,6 +87,7 @@ import org.schabi.newpipe.util.ThemeHelper.getGridSpanCountStreams
import org.schabi.newpipe.util.ThemeHelper.shouldUseGridLayout
import java.time.OffsetDateTime
import java.util.ArrayList
+import java.util.function.Consumer
class FeedFragment : BaseStateFragment() {
private var _feedBinding: FragmentFeedBinding? = null
@@ -97,6 +109,8 @@ class FeedFragment : BaseStateFragment() {
private var updateListViewModeOnResume = false
private var isRefreshing = false
+ private var lastNewItemsCount = 0
+
init {
setHasOptionsMenu(true)
}
@@ -126,8 +140,9 @@ class FeedFragment : BaseStateFragment() {
_feedBinding = FragmentFeedBinding.bind(rootView)
super.onViewCreated(rootView, savedInstanceState)
- val factory = FeedViewModel.Factory(requireContext(), groupId, showPlayedItems)
+ val factory = FeedViewModel.Factory(requireContext(), groupId)
viewModel = ViewModelProvider(this, factory).get(FeedViewModel::class.java)
+ showPlayedItems = viewModel.getShowPlayedItemsFromPreferences()
viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(::handleResult) })
groupAdapter = GroupieAdapter().apply {
@@ -135,6 +150,20 @@ class FeedFragment : BaseStateFragment() {
setOnItemLongClickListener(listenerStreamItem)
}
+ feedBinding.itemsList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ // Check if we scrolled to the top
+ if (newState == RecyclerView.SCROLL_STATE_IDLE &&
+ !recyclerView.canScrollVertically(-1)
+ ) {
+
+ if (tryGetNewItemsLoadedButton()?.isVisible == true) {
+ hideNewItemsLoaded(true)
+ }
+ }
+ }
+ })
+
feedBinding.itemsList.adapter = groupAdapter
setupListViewMode()
}
@@ -158,7 +187,7 @@ class FeedFragment : BaseStateFragment() {
}
}
- fun setupListViewMode() {
+ private fun setupListViewMode() {
// does everything needed to setup the layouts for grid or list modes
groupAdapter.spanCount = if (shouldUseGridLayout(context)) getGridSpanCountStreams(context) else 1
feedBinding.itemsList.layoutManager = GridLayoutManager(requireContext(), groupAdapter.spanCount).apply {
@@ -170,6 +199,10 @@ class FeedFragment : BaseStateFragment() {
super.initListeners()
feedBinding.refreshRootView.setOnClickListener { reloadContent() }
feedBinding.swipeRefreshLayout.setOnRefreshListener { reloadContent() }
+ feedBinding.newItemsLoadedButton.setOnClickListener {
+ hideNewItemsLoaded(true)
+ feedBinding.itemsList.scrollToPosition(0)
+ }
}
// /////////////////////////////////////////////////////////////////////////
@@ -213,6 +246,7 @@ class FeedFragment : BaseStateFragment() {
showPlayedItems = !item.isChecked
updateTogglePlayedItemsButton(item)
viewModel.togglePlayedItems(showPlayedItems)
+ viewModel.saveShowPlayedItemsToPreferences(showPlayedItems)
}
return super.onOptionsItemSelected(item)
@@ -236,6 +270,9 @@ class FeedFragment : BaseStateFragment() {
}
override fun onDestroyView() {
+ // Ensure that all animations are canceled
+ feedBinding.newItemsLoadedButton?.clearAnimation()
+
feedBinding.itemsList.adapter = null
_feedBinding = null
super.onDestroyView()
@@ -355,13 +392,7 @@ class FeedFragment : BaseStateFragment() {
}
// show "mark as watched" only when watch history is enabled
- val isWatchHistoryEnabled = PreferenceManager
- .getDefaultSharedPreferences(context)
- .getBoolean(getString(R.string.enable_watch_history_key), false)
- if (item.streamType != StreamType.AUDIO_LIVE_STREAM &&
- item.streamType != StreamType.LIVE_STREAM &&
- isWatchHistoryEnabled
- ) {
+ if (StreamDialogEntry.shouldAddMarkAsWatched(item.streamType, context)) {
entries.add(
StreamDialogEntry.mark_as_watched
)
@@ -404,7 +435,17 @@ class FeedFragment : BaseStateFragment() {
}
loadedState.items.forEach { it.itemVersion = itemVersion }
- groupAdapter.updateAsync(loadedState.items, false, null)
+ // 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)
+ }
+ }
+ )
listState?.run {
feedBinding.itemsList.layoutManager?.onRestoreInstanceState(listState)
@@ -464,7 +505,7 @@ class FeedFragment : BaseStateFragment() {
errors.subList(i + 1, errors.size)
)
},
- { throwable -> throwable.printStackTrace() }
+ { throwable -> Log.e(TAG, "Unable to process", throwable) }
)
return // this will be called on the remaining errors by handleFeedNotAvailable()
}
@@ -526,6 +567,125 @@ class FeedFragment : BaseStateFragment() {
)
}
+ /**
+ * Highlights all items that are after the specified time
+ */
+ private fun highlightNewItemsAfter(updateTime: OffsetDateTime) {
+ var highlightCount = 0
+
+ var doCheck = true
+
+ for (i in 0 until groupAdapter.itemCount) {
+ val item = groupAdapter.getItem(i) as StreamItem
+
+ var typeface = Typeface.DEFAULT
+ var backgroundSupplier = { ctx: Context ->
+ resolveDrawable(ctx, R.attr.selectableItemBackground)
+ }
+ if (doCheck) {
+ // If the uploadDate is null or true we should highlight the item
+ if (item.streamWithState.stream.uploadDate?.isAfter(updateTime) != false) {
+ highlightCount++
+
+ typeface = Typeface.DEFAULT_BOLD
+ backgroundSupplier = { ctx: Context ->
+ // Merge the drawables together. Otherwise we would lose the "select" effect
+ LayerDrawable(
+ arrayOf(
+ resolveDrawable(ctx, R.attr.dashed_border),
+ resolveDrawable(ctx, R.attr.selectableItemBackground)
+ )
+ )
+ }
+ } else {
+ // Decreases execution time due to the order of the items (newest always on top)
+ // Once a item is is before the updateTime we can skip all following items
+ doCheck = false
+ }
+ }
+
+ // The highlighter has to be always set
+ // When it's only set on items that are highlighted it will highlight all items
+ // due to the fact that itemRoot is getting recycled
+ item.execBindEnd = Consumer { viewBinding ->
+ val context = viewBinding.itemRoot.context
+ viewBinding.itemRoot.background = backgroundSupplier.invoke(context)
+ viewBinding.itemVideoTitleView.typeface = typeface
+ }
+ }
+
+ // Force updates all items so that the highlighting is correct
+ // If this isn't done visible items that are already highlighted will stay in a highlighted
+ // state until the user scrolls them out of the visible area which causes a update/bind-call
+ groupAdapter.notifyItemRangeChanged(
+ 0,
+ minOf(groupAdapter.itemCount, maxOf(highlightCount, lastNewItemsCount))
+ )
+
+ if (highlightCount > 0) {
+ showNewItemsLoaded()
+ }
+
+ lastNewItemsCount = highlightCount
+ }
+
+ private fun resolveDrawable(context: Context, @AttrRes attrResId: Int): Drawable? {
+ return androidx.core.content.ContextCompat.getDrawable(
+ context,
+ android.util.TypedValue().apply {
+ context.theme.resolveAttribute(
+ attrResId,
+ this,
+ true
+ )
+ }.resourceId
+ )
+ }
+
+ private fun showNewItemsLoaded() {
+ tryGetNewItemsLoadedButton()?.clearAnimation()
+ tryGetNewItemsLoadedButton()
+ ?.slideUp(
+ 250L,
+ delay = 100,
+ execOnEnd = {
+ // Disabled animations would result in immediately hiding the button
+ // after it showed up
+ if (DeviceUtils.hasAnimationsAnimatorDurationEnabled(context)) {
+ // Hide the new items-"popup" after 10s
+ hideNewItemsLoaded(true, 10000)
+ }
+ }
+ )
+ }
+
+ private fun hideNewItemsLoaded(animate: Boolean, delay: Long = 0) {
+ tryGetNewItemsLoadedButton()?.clearAnimation()
+ if (animate) {
+ tryGetNewItemsLoadedButton()?.animate(
+ false,
+ 200,
+ delay = delay,
+ execOnEnd = {
+ // Make the layout invisible so that the onScroll toTop method
+ // only does necessary work
+ tryGetNewItemsLoadedButton()?.isVisible = false
+ }
+ )
+ } else {
+ tryGetNewItemsLoadedButton()?.isVisible = false
+ }
+ }
+
+ /**
+ * The view/button can be disposed/set to null under certain circumstances.
+ * E.g. when the animation is still in progress but the view got destroyed.
+ * This method is a helper for such states and can be used in affected code blocks.
+ */
+ private fun tryGetNewItemsLoadedButton(): Button? {
+ return _feedBinding?.newItemsLoadedButton
+ }
+
// /////////////////////////////////////////////////////////////////////////
// Load Service Handling
// /////////////////////////////////////////////////////////////////////////
@@ -533,6 +693,8 @@ class FeedFragment : BaseStateFragment() {
override fun doInitialLoadLogic() {}
override fun reloadContent() {
+ hideNewItemsLoaded(false)
+
getActivity()?.startService(
Intent(requireContext(), FeedLoadService::class.java).apply {
putExtra(FeedLoadService.EXTRA_GROUP_ID, groupId)
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 8bdf412b5a2..2cbf9ad05b0 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
@@ -1,15 +1,18 @@
package org.schabi.newpipe.local.feed
import android.content.Context
+import androidx.core.content.edit
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import androidx.preference.PreferenceManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.functions.Function4
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
+import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.database.stream.StreamWithState
import org.schabi.newpipe.local.feed.item.StreamItem
@@ -23,19 +26,16 @@ import java.time.OffsetDateTime
import java.util.concurrent.TimeUnit
class FeedViewModel(
- applicationContext: Context,
+ private val applicationContext: Context,
groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
initialShowPlayedItems: Boolean = true
) : ViewModel() {
private var feedDatabaseManager: FeedDatabaseManager = FeedDatabaseManager(applicationContext)
private val toggleShowPlayedItems = BehaviorProcessor.create()
- private val streamItems = toggleShowPlayedItems
+ private val toggleShowPlayedItemsFlowable = toggleShowPlayedItems
.startWithItem(initialShowPlayedItems)
.distinctUntilChanged()
- .switchMap { showPlayedItems ->
- feedDatabaseManager.getStreams(groupId, showPlayedItems)
- }
private val mutableStateLiveData = MutableLiveData()
val stateLiveData: LiveData = mutableStateLiveData
@@ -43,17 +43,28 @@ class FeedViewModel(
private var combineDisposable = Flowable
.combineLatest(
FeedEventManager.events(),
- streamItems,
+ toggleShowPlayedItemsFlowable,
feedDatabaseManager.notLoadedCount(groupId),
feedDatabaseManager.oldestSubscriptionUpdate(groupId),
- Function4 { t1: FeedEventManager.Event, t2: List,
+ Function4 { t1: FeedEventManager.Event, t2: Boolean,
t3: Long, t4: List ->
- return@Function4 CombineResultHolder(t1, t2, t3, t4.firstOrNull())
+ return@Function4 CombineResultEventHolder(t1, t2, t3, t4.firstOrNull())
}
)
.throttleLatest(DEFAULT_THROTTLE_TIMEOUT, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
+ .observeOn(Schedulers.io())
+ .map { (event, showPlayedItems, notLoadedCount, oldestUpdate) ->
+ var streamItems = if (event is SuccessResultEvent || event is IdleEvent)
+ feedDatabaseManager
+ .getStreams(groupId, showPlayedItems)
+ .blockingGet(arrayListOf())
+ else
+ arrayListOf()
+
+ CombineResultDataHolder(event, streamItems, notLoadedCount, oldestUpdate)
+ }
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (event, listFromDB, notLoadedCount, oldestUpdate) ->
mutableStateLiveData.postValue(
@@ -75,20 +86,50 @@ class FeedViewModel(
combineDisposable.dispose()
}
- private data class CombineResultHolder(val t1: FeedEventManager.Event, val t2: List, val t3: Long, val t4: OffsetDateTime?)
+ private data class CombineResultEventHolder(
+ val t1: FeedEventManager.Event,
+ val t2: Boolean,
+ val t3: Long,
+ val t4: OffsetDateTime?
+ )
+
+ private data class CombineResultDataHolder(
+ val t1: FeedEventManager.Event,
+ val t2: List,
+ val t3: Long,
+ val t4: OffsetDateTime?
+ )
fun togglePlayedItems(showPlayedItems: Boolean) {
toggleShowPlayedItems.onNext(showPlayedItems)
}
+ fun saveShowPlayedItemsToPreferences(showPlayedItems: Boolean) =
+ PreferenceManager.getDefaultSharedPreferences(applicationContext).edit {
+ this.putBoolean(applicationContext.getString(R.string.feed_show_played_items_key), showPlayedItems)
+ this.apply()
+ }
+
+ fun getShowPlayedItemsFromPreferences() = getShowPlayedItemsFromPreferences(applicationContext)
+
+ companion object {
+ private fun getShowPlayedItemsFromPreferences(context: Context) =
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(context.getString(R.string.feed_show_played_items_key), true)
+ }
+
class Factory(
private val context: Context,
- private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID,
- private val showPlayedItems: Boolean
+ private val groupId: Long = FeedGroupEntity.GROUP_ALL_ID
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): T {
- return FeedViewModel(context.applicationContext, groupId, showPlayedItems) as T
+ return FeedViewModel(
+ context.applicationContext,
+ groupId,
+ // Read initial value from preferences
+ getShowPlayedItemsFromPreferences(context.applicationContext)
+ ) as T
}
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
index 0d2caf126d8..217e3f3e3c5 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/item/StreamItem.kt
@@ -19,6 +19,7 @@ import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.PicassoHelper
import org.schabi.newpipe.util.StreamTypeUtil
import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
data class StreamItem(
val streamWithState: StreamWithState,
@@ -31,6 +32,12 @@ data class StreamItem(
private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressMillis
+ /**
+ * Will be executed at the end of the [StreamItem.bind] (with (ListStreamItemBinding,Int)).
+ * Can be used e.g. for highlighting a item.
+ */
+ var execBindEnd: Consumer? = null
+
override fun getId(): Long = stream.uid
enum class ItemVersion { NORMAL, MINI, GRID }
@@ -97,6 +104,8 @@ data class StreamItem(
viewBinding.itemAdditionalDetails.text =
getStreamInfoDetailLine(viewBinding.itemAdditionalDetails.context)
}
+
+ execBindEnd?.accept(viewBinding)
}
override fun isLongClickable() = when (stream.streamType) {
diff --git a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
index 823e56d9e4e..d94088cd031 100644
--- a/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
+++ b/app/src/main/java/org/schabi/newpipe/local/history/HistoryRecordManager.java
@@ -120,19 +120,11 @@ public Maybe markAsWatched(final StreamInfoItem info) {
}
// Update the stream progress to the full duration of the video
- final List states = streamStateTable.getState(streamId)
- .blockingFirst();
- if (!states.isEmpty()) {
- final StreamStateEntity entity = states.get(0);
- entity.setProgressMillis(duration * 1000);
- streamStateTable.update(entity);
- } else {
- final StreamStateEntity entity = new StreamStateEntity(
- streamId,
- duration * 1000
- );
- streamStateTable.insert(entity);
- }
+ final StreamStateEntity entity = new StreamStateEntity(
+ streamId,
+ duration * 1000
+ );
+ streamStateTable.upsert(entity);
// Add a history entry
final StreamHistoryEntity latestEntry = streamHistoryTable.getLatestEntry(streamId);
@@ -334,9 +326,9 @@ public Single> loadStreamStateBatch(final List
.getState(entities.get(0).getUid()).blockingFirst();
if (states.isEmpty()) {
result.add(null);
- continue;
+ } else {
+ result.add(states.get(0));
}
- result.add(states.get(0));
}
return result;
}).subscribeOn(Schedulers.io());
@@ -362,9 +354,9 @@ public Single> loadLocalStreamStateBatch(
.blockingFirst();
if (states.isEmpty()) {
result.add(null);
- continue;
+ } else {
+ result.add(states.get(0));
}
- result.add(states.get(0));
}
return result;
}).subscribeOn(Schedulers.io());
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 9632b47f719..43a5fcf3c6a 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
@@ -101,9 +101,9 @@ public View onCreateView(@NonNull final LayoutInflater inflater,
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (activity != null && isVisibleToUser) {
+ public void onResume() {
+ super.onResume();
+ if (activity != null) {
setTitle(activity.getString(R.string.title_activity_history));
}
}
@@ -366,6 +366,16 @@ private void showStreamDialog(final StreamStatisticsEntry item) {
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);
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 21da9e57104..87d913b3b06 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
@@ -709,8 +709,8 @@ public boolean onMove(@NonNull final RecyclerView recyclerView,
return false;
}
- final int sourceIndex = source.getAdapterPosition();
- final int targetIndex = target.getAdapterPosition();
+ final int sourceIndex = source.getBindingAdapterPosition();
+ final int targetIndex = target.getBindingAdapterPosition();
final boolean isSwapped = itemListAdapter.swapItems(sourceIndex, targetIndex);
if (isSwapped) {
saveChanges();
@@ -782,6 +782,16 @@ protected void showStreamItemDialog(final PlaylistStreamEntry item) {
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);
diff --git a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java
index c4d088e3910..675799586fe 100644
--- a/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/local/subscription/SubscriptionsImportFragment.java
@@ -97,11 +97,9 @@ public void onCreate(final Bundle savedInstanceState) {
}
@Override
- public void setUserVisibleHint(final boolean isVisibleToUser) {
- super.setUserVisibleHint(isVisibleToUser);
- if (isVisibleToUser) {
- setTitle(getString(R.string.import_title));
- }
+ public void onResume() {
+ super.onResume();
+ setTitle(getString(R.string.import_title));
}
@Nullable
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 58547afd222..50e8aae6a80 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
@@ -112,8 +112,8 @@ class FeedGroupReorderDialog : DialogFragment() {
source: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
- val sourceIndex = source.adapterPosition
- val targetIndex = target.adapterPosition
+ val sourceIndex = source.bindingAdapterPosition
+ val targetIndex = target.bindingAdapterPosition
groupAdapter.notifyItemMoved(sourceIndex, targetIndex)
Collections.swap(groupOrderedIdList, sourceIndex, targetIndex)
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 0976aa4fb87..e0c5ab08366 100644
--- a/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
+++ b/app/src/main/java/org/schabi/newpipe/player/PlayQueueActivity.java
@@ -23,11 +23,11 @@
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.PlaylistAppendDialog;
-import org.schabi.newpipe.local.dialog.PlaylistCreationDialog;
+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;
@@ -43,6 +43,7 @@
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;
@@ -452,12 +453,12 @@ private void appendAllToPlaylist() {
}
}
- private void openPlaylistAppendDialog(final List playlist) {
- final PlaylistAppendDialog d = PlaylistAppendDialog.fromPlayQueueItems(playlist);
-
- PlaylistAppendDialog.onPlaylistFound(getApplicationContext(),
- () -> d.show(getSupportFragmentManager(), TAG),
- () -> PlaylistCreationDialog.newInstance(d).show(getSupportFragmentManager(), TAG));
+ private void openPlaylistAppendDialog(final List playQueueItems) {
+ PlaylistDialog.createCorrespondingDialog(
+ getApplicationContext(),
+ playQueueItems.stream().map(StreamEntity::new).collect(Collectors.toList()),
+ dialog -> dialog.show(getSupportFragmentManager(), TAG)
+ );
}
////////////////////////////////////////////////////////////////////////////
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 22e66e793dd..2d8c1a83017 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -1,12 +1,13 @@
package org.schabi.newpipe.player;
-import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AD_INSERTION;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_AUTO_TRANSITION;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_INTERNAL;
-import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_PERIOD_TRANSITION;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_REMOVE;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK;
import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT;
+import static com.google.android.exoplayer2.Player.DISCONTINUITY_REASON_SKIP;
import static com.google.android.exoplayer2.Player.DiscontinuityReason;
-import static com.google.android.exoplayer2.Player.EventListener;
+import static com.google.android.exoplayer2.Player.Listener;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
@@ -96,7 +97,6 @@
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
-import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -116,6 +116,7 @@
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.PlaybackParameters;
+import com.google.android.exoplayer2.Player.PositionInfo;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
@@ -123,13 +124,14 @@
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.CaptionStyleCompat;
+import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
+import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.util.Util;
-import com.google.android.exoplayer2.video.VideoListener;
+import com.google.android.exoplayer2.video.VideoSize;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
@@ -163,6 +165,7 @@
import org.schabi.newpipe.player.playback.PlaybackListener;
import org.schabi.newpipe.player.playback.PlayerMediaSession;
import org.schabi.newpipe.player.playback.SurfaceHolderCallback;
+import org.schabi.newpipe.player.playererror.PlayerErrorHandler;
import org.schabi.newpipe.player.playqueue.PlayQueue;
import org.schabi.newpipe.player.playqueue.PlayQueueAdapter;
import org.schabi.newpipe.player.playqueue.PlayQueueItem;
@@ -174,12 +177,12 @@
import org.schabi.newpipe.player.resolver.VideoPlaybackResolver;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHelper;
import org.schabi.newpipe.player.seekbarpreview.SeekbarPreviewThumbnailHolder;
-import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.DeviceUtils;
import org.schabi.newpipe.util.ListHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.PicassoHelper;
import org.schabi.newpipe.util.SerializedCache;
+import org.schabi.newpipe.util.StreamTypeUtil;
import org.schabi.newpipe.util.external_communication.KoreUtils;
import org.schabi.newpipe.util.external_communication.ShareUtils;
import org.schabi.newpipe.views.ExpandableSurfaceView;
@@ -197,9 +200,8 @@
import io.reactivex.rxjava3.disposables.SerialDisposable;
public final class Player implements
- EventListener,
PlaybackListener,
- VideoListener,
+ Listener,
SeekBar.OnSeekBarChangeListener,
View.OnClickListener,
PopupMenu.OnMenuItemClickListener,
@@ -266,7 +268,7 @@ public final class Player implements
@Nullable private MediaSourceTag currentMetadata;
@Nullable private Bitmap currentThumbnail;
- @Nullable private Toast errorToast;
+ @NonNull private PlayerErrorHandler playerErrorHandler;
/*//////////////////////////////////////////////////////////////////////////
// Player
@@ -411,6 +413,8 @@ public Player(@NonNull final MainPlayer service) {
videoResolver = new VideoPlaybackResolver(context, dataSource, getQualityResolver());
audioResolver = new AudioPlaybackResolver(context, dataSource);
+ playerErrorHandler = new PlayerErrorHandler(context);
+
windowManager = ContextCompat.getSystemService(context, WindowManager.class);
}
@@ -501,10 +505,6 @@ private void initPlayer(final boolean playOnReady) {
// Setup video view
setupVideoSurface();
- simpleExoPlayer.addVideoListener(this);
-
- // Setup subtitle view
- simpleExoPlayer.addTextOutput(binding.subtitleView);
// enable media tunneling
if (DEBUG && PreferenceManager.getDefaultSharedPreferences(context)
@@ -513,7 +513,7 @@ private void initPlayer(final boolean playOnReady) {
+ "media tunneling disabled in debug preferences");
} else if (DeviceUtils.shouldSupportMediaTunneling()) {
trackSelector.setParameters(trackSelector.buildUponParameters()
- .setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context)));
+ .setTunnelingEnabled(true));
} else if (DEBUG) {
Log.d(TAG, "[" + Util.DEVICE_DEBUG_INFO + "] does not support media tunneling");
}
@@ -695,7 +695,7 @@ && isPlaybackResumeEnabled(this)
},
error -> {
if (DEBUG) {
- error.printStackTrace();
+ Log.w(TAG, "Failed to start playback", error);
}
// In case any error we can start playback without history
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
@@ -774,6 +774,8 @@ private void initPlayback(@NonNull final PlayQueue queue,
destroyPlayer();
initPlayer(playOnReady);
setRepeatMode(repeatMode);
+ // #6825 - Ensure that the shuffle-button is in the correct state on the UI
+ setShuffleButton(binding.shuffleButton, simpleExoPlayer.getShuffleModeEnabled());
setPlaybackParameters(playbackSpeed, playbackPitch, playbackSkipSilence);
playQueue = queue;
@@ -807,7 +809,6 @@ private void destroyPlayer() {
if (!exoPlayerIsNull()) {
simpleExoPlayer.removeListener(this);
- simpleExoPlayer.removeVideoListener(this);
simpleExoPlayer.stop();
simpleExoPlayer.release();
}
@@ -858,10 +859,10 @@ public void setRecovery() {
final int queuePos = playQueue.getIndex();
final long windowPos = simpleExoPlayer.getCurrentPosition();
+ final long duration = simpleExoPlayer.getDuration();
- if (windowPos > 0 && windowPos <= simpleExoPlayer.getDuration()) {
- setRecovery(queuePos, windowPos);
- }
+ // No checks due to https://github.com/TeamNewPipe/NewPipe/pull/7195#issuecomment-962624380
+ setRecovery(queuePos, Math.max(0, Math.min(windowPos, duration)));
}
private void setRecovery(final int queuePos, final long windowPos) {
@@ -896,7 +897,7 @@ public void onPlaybackShutdown() {
public void smoothStopPlayer() {
// Pausing would make transition from one stream to a new stream not smooth, so only stop
- simpleExoPlayer.stop(false);
+ simpleExoPlayer.stop();
}
//endregion
@@ -2435,7 +2436,9 @@ public void onPlaybackParametersChanged(@NonNull final PlaybackParameters playba
}
@Override
- public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuityReason) {
+ public void onPositionDiscontinuity(
+ final PositionInfo oldPosition, final PositionInfo newPosition,
+ @DiscontinuityReason final int discontinuityReason) {
if (DEBUG) {
Log.d(TAG, "ExoPlayer - onPositionDiscontinuity() called with "
+ "discontinuityReason = [" + discontinuityReason + "]");
@@ -2447,7 +2450,8 @@ public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuity
// Refresh the playback if there is a transition to the next video
final int newWindowIndex = simpleExoPlayer.getCurrentWindowIndex();
switch (discontinuityReason) {
- case DISCONTINUITY_REASON_PERIOD_TRANSITION:
+ case DISCONTINUITY_REASON_AUTO_TRANSITION:
+ case DISCONTINUITY_REASON_REMOVE:
// When player is in single repeat mode and a period transition occurs,
// we need to register a view count here since no metadata has changed
if (getRepeatMode() == REPEAT_MODE_ONE && newWindowIndex == playQueue.getIndex()) {
@@ -2468,7 +2472,7 @@ public void onPositionDiscontinuity(@DiscontinuityReason final int discontinuity
playQueue.setIndex(newWindowIndex);
}
break;
- case DISCONTINUITY_REASON_AD_INSERTION:
+ case DISCONTINUITY_REASON_SKIP:
break; // only makes Android Studio linter happy, as there are no ads
}
@@ -2480,6 +2484,11 @@ public void onRenderedFirstFrame() {
//TODO check if this causes black screen when switching to fullscreen
animate(binding.surfaceForeground, false, DEFAULT_CONTROLS_DURATION);
}
+
+ @Override
+ public void onCues(final List cues) {
+ binding.subtitleView.onCues(cues);
+ }
//endregion
@@ -2501,34 +2510,37 @@ public void onRenderedFirstFrame() {
*
*
* @see #processSourceError(IOException)
- * @see com.google.android.exoplayer2.Player.EventListener#onPlayerError(ExoPlaybackException)
+ * @see com.google.android.exoplayer2.Player.Listener#onPlayerError(ExoPlaybackException)
*/
@Override
public void onPlayerError(@NonNull final ExoPlaybackException error) {
- if (DEBUG) {
- Log.d(TAG, "ExoPlayer - onPlayerError() called with: " + "error = [" + error + "]");
- }
- if (errorToast != null) {
- errorToast.cancel();
- errorToast = null;
- }
+ Log.e(TAG, "ExoPlayer - onPlayerError() called with:", error);
saveStreamProgressState();
switch (error.type) {
case ExoPlaybackException.TYPE_SOURCE:
processSourceError(error.getSourceException());
- showStreamError(error);
+ playerErrorHandler.showPlayerError(
+ error,
+ currentMetadata.getMetadata(),
+ R.string.player_stream_failure);
break;
case ExoPlaybackException.TYPE_UNEXPECTED:
- showRecoverableError(error);
+ playerErrorHandler.showPlayerError(
+ error,
+ currentMetadata.getMetadata(),
+ R.string.player_recoverable_failure);
setRecovery();
reloadPlayQueueManager();
break;
case ExoPlaybackException.TYPE_REMOTE:
case ExoPlaybackException.TYPE_RENDERER:
default:
- showUnrecoverableError(error);
+ playerErrorHandler.showPlayerError(
+ error,
+ currentMetadata.getMetadata(),
+ R.string.player_unrecoverable_failure);
onPlaybackShutdown();
break;
}
@@ -2550,37 +2562,6 @@ private void processSourceError(final IOException error) {
playQueue.error();
}
}
-
- private void showStreamError(final Exception exception) {
- exception.printStackTrace();
-
- if (errorToast == null) {
- errorToast = Toast
- .makeText(context, R.string.player_stream_failure, Toast.LENGTH_SHORT);
- errorToast.show();
- }
- }
-
- private void showRecoverableError(final Exception exception) {
- exception.printStackTrace();
-
- if (errorToast == null) {
- errorToast = Toast
- .makeText(context, R.string.player_recoverable_failure, Toast.LENGTH_SHORT);
- errorToast.show();
- }
- }
-
- private void showUnrecoverableError(final Exception exception) {
- exception.printStackTrace();
-
- if (errorToast != null) {
- errorToast.cancel();
- }
- errorToast = Toast
- .makeText(context, R.string.player_unrecoverable_failure, Toast.LENGTH_SHORT);
- errorToast.show();
- }
//endregion
@@ -3865,19 +3846,17 @@ void onResizeClicked() {
}
@Override // exoplayer listener
- public void onVideoSizeChanged(final int width, final int height,
- final int unappliedRotationDegrees,
- final float pixelWidthHeightRatio) {
+ public void onVideoSizeChanged(final VideoSize videoSize) {
if (DEBUG) {
Log.d(TAG, "onVideoSizeChanged() called with: "
- + "width / height = [" + width + " / " + height
- + " = " + (((float) width) / height) + "], "
- + "unappliedRotationDegrees = [" + unappliedRotationDegrees + "], "
- + "pixelWidthHeightRatio = [" + pixelWidthHeightRatio + "]");
+ + "width / height = [" + videoSize.width + " / " + videoSize.height
+ + " = " + (((float) videoSize.width) / videoSize.height) + "], "
+ + "unappliedRotationDegrees = [" + videoSize.unappliedRotationDegrees + "], "
+ + "pixelWidthHeightRatio = [" + videoSize.pixelWidthHeightRatio + "]");
}
- binding.surfaceView.setAspectRatio(((float) width) / height);
- isVerticalVideo = width < height;
+ binding.surfaceView.setAspectRatio(((float) videoSize.width) / videoSize.height);
+ isVerticalVideo = videoSize.width < videoSize.height;
if (globalScreenOrientationLocked(context)
&& isFullscreen
@@ -4182,8 +4161,7 @@ private boolean isLive() {
} catch (@NonNull 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.getMessage());
- e.printStackTrace();
+ Log.d(TAG, "player.isCurrentWindowDynamic() failed: ", e);
}
return false;
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
index 998324c9c66..e55c596b800 100644
--- a/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
+++ b/app/src/main/java/org/schabi/newpipe/player/event/PlayerGestureListener.java
@@ -69,26 +69,18 @@ public void onSingleTap(@NotNull final MainPlayer.PlayerType playerType) {
if (DEBUG) {
Log.d(TAG, "onSingleTap called with playerType = [" + player.getPlayerType() + "]");
}
- if (playerType == MainPlayer.PlayerType.POPUP) {
- if (player.isControlsVisible()) {
- player.hideControls(100, 100);
- } else {
- player.getPlayPauseButton().requestFocus();
- player.showControlsThenHide();
- }
-
- } else /* playerType == MainPlayer.PlayerType.VIDEO */ {
+ if (player.isControlsVisible()) {
+ player.hideControls(150, 0);
+ return;
+ }
+ // -- Controls are not visible --
- if (player.isControlsVisible()) {
- player.hideControls(150, 0);
- } else {
- if (player.getCurrentState() == Player.STATE_COMPLETED) {
- player.showControls(0);
- } else {
- player.showControlsThenHide();
- }
- }
+ // When player is completed show controls and don't hide them later
+ if (player.getCurrentState() == Player.STATE_COMPLETED) {
+ player.showControls(0);
+ } else {
+ player.showControlsThenHide();
}
}
@@ -103,6 +95,8 @@ public void onScroll(@NotNull final MainPlayer.PlayerType playerType,
+ player.getPlayerType() + "], portion = [" + portion + "]");
}
if (playerType == MainPlayer.PlayerType.VIDEO) {
+
+ // -- Brightness and Volume control --
final boolean isBrightnessGestureEnabled =
PlayerHelper.isBrightnessGestureEnabled(service);
final boolean isVolumeGestureEnabled = PlayerHelper.isVolumeGestureEnabled(service);
@@ -121,15 +115,14 @@ public void onScroll(@NotNull final MainPlayer.PlayerType playerType,
}
} else /* MainPlayer.PlayerType.POPUP */ {
+
+ // -- Determine if the ClosingOverlayView (red X) has to be shown or hidden --
final View closingOverlayView = player.getClosingOverlayView();
- if (player.isInsideClosingRadius(movingEvent)) {
- if (closingOverlayView.getVisibility() == View.GONE) {
- animate(closingOverlayView, true, 200);
- }
- } else {
- if (closingOverlayView.getVisibility() == View.VISIBLE) {
- animate(closingOverlayView, false, 200);
- }
+ final boolean showClosingOverlayView = player.isInsideClosingRadius(movingEvent);
+ // Check if an view is in expected state and if not animate it into the correct state
+ final int expectedVisibility = showClosingOverlayView ? View.VISIBLE : View.GONE;
+ if (closingOverlayView.getVisibility() != expectedVisibility) {
+ animate(closingOverlayView, showClosingOverlayView, 200);
}
}
}
@@ -210,11 +203,12 @@ public void onScrollEnd(@NotNull final MainPlayer.PlayerType playerType,
Log.d(TAG, "onScrollEnd called with playerType = ["
+ player.getPlayerType() + "]");
}
- if (playerType == MainPlayer.PlayerType.VIDEO) {
- if (DEBUG) {
- Log.d(TAG, "onScrollEnd() called");
- }
+ if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
+ player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
+ }
+
+ if (playerType == MainPlayer.PlayerType.VIDEO) {
if (player.getVolumeRelativeLayout().getVisibility() == View.VISIBLE) {
animate(player.getVolumeRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
@@ -223,15 +217,7 @@ public void onScrollEnd(@NotNull final MainPlayer.PlayerType playerType,
animate(player.getBrightnessRelativeLayout(), false, 200, SCALE_AND_ALPHA,
200);
}
-
- if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
- player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
- } else {
- if (player.isControlsVisible() && player.getCurrentState() == STATE_PLAYING) {
- player.hideControls(DEFAULT_CONTROLS_DURATION, DEFAULT_CONTROLS_HIDE_TIME);
- }
-
+ } else /* Popup-Player */ {
if (player.isInsideClosingRadius(event)) {
player.closePopup();
} else if (!player.isPopupClosing()) {
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 2e2fda86c57..b36f9f23488 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
@@ -16,7 +16,6 @@
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.analytics.AnalyticsListener;
-import com.google.android.exoplayer2.decoder.DecoderCounters;
public class AudioReactor implements AudioManager.OnAudioFocusChangeListener, AnalyticsListener {
@@ -150,15 +149,9 @@ public void onAnimationEnd(final Animator animation) {
//////////////////////////////////////////////////////////////////////////*/
@Override
- public void onAudioSessionId(final EventTime eventTime, final int audioSessionId) {
+ public void onAudioSessionIdChanged(final EventTime eventTime, final int audioSessionId) {
notifyAudioSessionUpdate(true, audioSessionId);
}
-
- @Override
- public void onAudioDisabled(final EventTime eventTime, final DecoderCounters counters) {
- notifyAudioSessionUpdate(false, player.getAudioSessionId());
- }
-
private void notifyAudioSessionUpdate(final boolean active, final int audioSessionId) {
if (!PlayerHelper.isUsingDSP()) {
return;
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
index 71cfcc818b7..ec0e4e4a72f 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/LoadController.java
@@ -1,81 +1,28 @@
package org.schabi.newpipe.player.helper;
import com.google.android.exoplayer2.DefaultLoadControl;
-import com.google.android.exoplayer2.LoadControl;
-import com.google.android.exoplayer2.Renderer;
-import com.google.android.exoplayer2.source.TrackGroupArray;
-import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
-import com.google.android.exoplayer2.upstream.Allocator;
-public class LoadController implements LoadControl {
+public class LoadController extends DefaultLoadControl {
public static final String TAG = "LoadController";
-
- private final long initialPlaybackBufferUs;
- private final LoadControl internalLoadControl;
private boolean preloadingEnabled = true;
- /*//////////////////////////////////////////////////////////////////////////
- // Default Load Control
- //////////////////////////////////////////////////////////////////////////*/
-
- public LoadController() {
- this(PlayerHelper.getPlaybackStartBufferMs());
- }
-
- private LoadController(final int initialPlaybackBufferMs) {
- this.initialPlaybackBufferUs = initialPlaybackBufferMs * 1000;
-
- final DefaultLoadControl.Builder builder = new DefaultLoadControl.Builder();
- builder.setBufferDurationsMs(
- DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
- DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
- initialPlaybackBufferMs,
- DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS);
- internalLoadControl = builder.build();
- }
-
- /*//////////////////////////////////////////////////////////////////////////
- // Custom behaviours
- //////////////////////////////////////////////////////////////////////////*/
-
@Override
public void onPrepared() {
preloadingEnabled = true;
- internalLoadControl.onPrepared();
- }
-
- @Override
- public void onTracksSelected(final Renderer[] renderers, final TrackGroupArray trackGroups,
- final TrackSelectionArray trackSelections) {
- internalLoadControl.onTracksSelected(renderers, trackGroups, trackSelections);
+ super.onPrepared();
}
@Override
public void onStopped() {
preloadingEnabled = true;
- internalLoadControl.onStopped();
+ super.onStopped();
}
@Override
public void onReleased() {
preloadingEnabled = true;
- internalLoadControl.onReleased();
- }
-
- @Override
- public Allocator getAllocator() {
- return internalLoadControl.getAllocator();
- }
-
- @Override
- public long getBackBufferDurationUs() {
- return internalLoadControl.getBackBufferDurationUs();
- }
-
- @Override
- public boolean retainBackBufferFromKeyframe() {
- return internalLoadControl.retainBackBufferFromKeyframe();
+ super.onReleased();
}
@Override
@@ -85,20 +32,10 @@ public boolean shouldContinueLoading(final long playbackPositionUs,
if (!preloadingEnabled) {
return false;
}
- return internalLoadControl.shouldContinueLoading(
+ return super.shouldContinueLoading(
playbackPositionUs, bufferedDurationUs, playbackSpeed);
}
- @Override
- public boolean shouldStartPlayback(final long bufferedDurationUs, final float playbackSpeed,
- final boolean rebuffering) {
- final boolean isInitialPlaybackBufferFilled
- = bufferedDurationUs >= this.initialPlaybackBufferUs * playbackSpeed;
- final boolean isInternalStartingPlayback = internalLoadControl
- .shouldStartPlayback(bufferedDurationUs, playbackSpeed, rebuffering);
- return isInitialPlaybackBufferFilled || isInternalStartingPlayback;
- }
-
public void disablePreloadingOfCurrentTrack() {
preloadingEnabled = false;
}
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 ef0d8402934..8d344c87788 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
@@ -179,9 +179,7 @@ private boolean checkIfMetadataShouldBeSet(
// If we got an album art check if the current set AlbumArt is null
if (optAlbumArt.isPresent() && getMetadataAlbumArt() == null) {
if (DEBUG) {
- if (getMetadataAlbumArt() == null) {
- Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
- }
+ Log.d(TAG, "N_getMetadataAlbumArt: thumb == null");
}
return true;
}
@@ -191,16 +189,19 @@ private boolean checkIfMetadataShouldBeSet(
}
+ @Nullable
private Bitmap getMetadataAlbumArt() {
return mediaSession.getController().getMetadata()
.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART);
}
+ @Nullable
private String getMetadataTitle() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_TITLE);
}
+ @Nullable
private String getMetadataArtist() {
return mediaSession.getController().getMetadata()
.getString(MediaMetadataCompat.METADATA_KEY_ARTIST);
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 bbe28192133..5139ef9cd60 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
@@ -1,5 +1,8 @@
package org.schabi.newpipe.player.helper;
+import static org.schabi.newpipe.player.Player.DEBUG;
+import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
+
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
@@ -18,9 +21,6 @@
import org.schabi.newpipe.R;
import org.schabi.newpipe.util.SliderStrategy;
-import static org.schabi.newpipe.player.Player.DEBUG;
-import static org.schabi.newpipe.util.Localization.assureCorrectAppLanguage;
-
public class PlaybackParameterDialog extends DialogFragment {
// Minimum allowable range in ExoPlayer
private static final double MINIMUM_PLAYBACK_VALUE = 0.10f;
@@ -157,7 +157,6 @@ public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) {
setupControlViews(view);
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(requireActivity())
- .setTitle(R.string.playback_speed_control)
.setView(view)
.setCancelable(true)
.setNegativeButton(R.string.cancel, (dialogInterface, i) ->
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 5fea4761bc0..b7584151d91 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
@@ -1,14 +1,18 @@
package org.schabi.newpipe.player.helper;
import android.content.Context;
+import android.os.Build;
import androidx.annotation.NonNull;
+import com.google.android.exoplayer2.source.MediaParserExtractorAdapter;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
+import com.google.android.exoplayer2.source.chunk.MediaParserChunkExtractor;
import com.google.android.exoplayer2.source.dash.DashMediaSource;
import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
+import com.google.android.exoplayer2.source.hls.MediaParserHlsMediaChunkExtractor;
import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource;
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource;
import com.google.android.exoplayer2.upstream.DataSource;
@@ -19,7 +23,7 @@
public class PlayerDataSource {
private static final int MANIFEST_MINIMUM_RETRY = 5;
private static final int EXTRACTOR_MINIMUM_RETRY = Integer.MAX_VALUE;
- private static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
+ public static final int LIVE_STREAM_EDGE_GAP_MILLIS = 10000;
private final DataSource.Factory cacheDataSourceFactory;
private final DataSource.Factory cachelessDataSourceFactory;
@@ -32,51 +36,83 @@ public PlayerDataSource(@NonNull final Context context, @NonNull final String us
}
public SsMediaSource.Factory getLiveSsMediaSourceFactory() {
- return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
- cachelessDataSourceFactory), cachelessDataSourceFactory)
+ return new SsMediaSource.Factory(
+ new DefaultSsChunkSource.Factory(cachelessDataSourceFactory),
+ cachelessDataSourceFactory
+ )
.setLoadErrorHandlingPolicy(
new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
.setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS);
}
public HlsMediaSource.Factory getLiveHlsMediaSourceFactory() {
- return new HlsMediaSource.Factory(cachelessDataSourceFactory)
- .setAllowChunklessPreparation(true)
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
+ final HlsMediaSource.Factory factory =
+ new HlsMediaSource.Factory(cachelessDataSourceFactory)
+ .setAllowChunklessPreparation(true)
+ .setLoadErrorHandlingPolicy(
+ new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
+ }
+
+ return factory;
}
public DashMediaSource.Factory getLiveDashMediaSourceFactory() {
- return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
- cachelessDataSourceFactory), cachelessDataSourceFactory)
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(cachelessDataSourceFactory),
+ cachelessDataSourceFactory
+ )
.setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY))
- .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true);
+ new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY));
}
- public SsMediaSource.Factory getSsMediaSourceFactory() {
- return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory(
- cacheDataSourceFactory), cacheDataSourceFactory);
+ private DefaultDashChunkSource.Factory getDefaultDashChunkSourceFactory(
+ final DataSource.Factory dataSourceFactory
+ ) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ return new DefaultDashChunkSource.Factory(
+ MediaParserChunkExtractor.FACTORY,
+ dataSourceFactory,
+ 1
+ );
+ }
+
+ return new DefaultDashChunkSource.Factory(dataSourceFactory);
}
public HlsMediaSource.Factory getHlsMediaSourceFactory() {
- return new HlsMediaSource.Factory(cacheDataSourceFactory);
+ final HlsMediaSource.Factory factory = new HlsMediaSource.Factory(cacheDataSourceFactory);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+ return factory;
+ }
+
+ // *** >= Android 11 / R / API 30 ***
+ return factory.setExtractorFactory(MediaParserHlsMediaChunkExtractor.FACTORY);
}
public DashMediaSource.Factory getDashMediaSourceFactory() {
- return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory(
- cacheDataSourceFactory), cacheDataSourceFactory);
+ return new DashMediaSource.Factory(
+ getDefaultDashChunkSourceFactory(cacheDataSourceFactory),
+ cacheDataSourceFactory
+ );
}
public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory() {
- return new ProgressiveMediaSource.Factory(cacheDataSourceFactory)
- .setLoadErrorHandlingPolicy(
- new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
- }
+ final ProgressiveMediaSource.Factory factory;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ factory = new ProgressiveMediaSource.Factory(
+ cacheDataSourceFactory,
+ MediaParserExtractorAdapter.FACTORY
+ );
+ } else {
+ factory = new ProgressiveMediaSource.Factory(cacheDataSourceFactory);
+ }
- public ProgressiveMediaSource.Factory getExtractorMediaSourceFactory(
- @NonNull final String key) {
- return getExtractorMediaSourceFactory().setCustomCacheKey(key);
+ return factory.setLoadErrorHandlingPolicy(
+ new DefaultLoadErrorHandlingPolicy(EXTRACTOR_MINIMUM_RETRY));
}
public SingleSampleMediaSource.Factory getSampleMediaSourceFactory() {
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 828833a8d9a..c51b6d5dde0 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
@@ -1,5 +1,18 @@
package org.schabi.newpipe.player.helper;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
+import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
+import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
+import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
+import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
+import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -21,11 +34,11 @@
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player.RepeatMode;
import com.google.android.exoplayer2.SeekParameters;
-import com.google.android.exoplayer2.text.CaptionStyleCompat;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode;
+import com.google.android.exoplayer2.ui.CaptionStyleCompat;
import com.google.android.exoplayer2.util.MimeTypes;
import org.schabi.newpipe.R;
@@ -57,19 +70,6 @@
import java.util.Set;
import java.util.concurrent.TimeUnit;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_OFF;
-import static com.google.android.exoplayer2.Player.REPEAT_MODE_ONE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-import static org.schabi.newpipe.player.Player.IDLE_WINDOW_FLAGS;
-import static org.schabi.newpipe.player.Player.PLAYER_TYPE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_ALWAYS;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_NEVER;
-import static org.schabi.newpipe.player.helper.PlayerHelper.AutoplayType.AUTOPLAY_TYPE_WIFI;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_BACKGROUND;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_NONE;
-import static org.schabi.newpipe.player.helper.PlayerHelper.MinimizeMode.MINIMIZE_ON_EXIT_MODE_POPUP;
-
public final class PlayerHelper {
private static final StringBuilder STRING_BUILDER = new StringBuilder();
private static final Formatter STRING_FORMATTER
@@ -305,14 +305,7 @@ public static long getPreferredFileSize() {
return 2 * 1024 * 1024L; // ExoPlayer CacheDataSink.MIN_RECOMMENDED_FRAGMENT_SIZE
}
- /**
- * @return the number of milliseconds the player buffers for before starting playback
- */
- public static int getPlaybackStartBufferMs() {
- return 500;
- }
-
- public static TrackSelection.Factory getQualitySelector() {
+ public static ExoTrackSelection.Factory getQualitySelector() {
return new AdaptiveTrackSelection.Factory(
1000,
AdaptiveTrackSelection.DEFAULT_MAX_DURATION_FOR_QUALITY_DECREASE_MS,
diff --git a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
index d70707fdbf1..389be70628e 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playback/CustomTrackSelector.java
@@ -13,7 +13,7 @@
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
-import com.google.android.exoplayer2.trackselection.TrackSelection;
+import com.google.android.exoplayer2.trackselection.ExoTrackSelection;
import com.google.android.exoplayer2.util.Assertions;
/**
@@ -28,7 +28,7 @@ public class CustomTrackSelector extends DefaultTrackSelector {
private String preferredTextLanguage;
public CustomTrackSelector(final Context context,
- final TrackSelection.Factory adaptiveTrackSelectionFactory) {
+ final ExoTrackSelection.Factory adaptiveTrackSelectionFactory) {
super(context, adaptiveTrackSelectionFactory);
}
@@ -50,7 +50,7 @@ public void setPreferredTextLanguage(@NonNull final String label) {
@Override
@Nullable
- protected Pair selectTextTrack(
+ protected Pair selectTextTrack(
final TrackGroupArray groups,
@NonNull final int[][] formatSupport,
@NonNull final Parameters params,
@@ -86,7 +86,7 @@ protected Pair selectTextTrack(
}
}
return selectedGroup == null ? null
- : Pair.create(new TrackSelection.Definition(selectedGroup, selectedTrackIndex),
+ : Pair.create(new ExoTrackSelection.Definition(selectedGroup, selectedTrackIndex),
Assertions.checkNotNull(selectedTrackScore));
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/playererror/PlayerErrorHandler.java b/app/src/main/java/org/schabi/newpipe/player/playererror/PlayerErrorHandler.java
new file mode 100644
index 00000000000..626200ae1b0
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/player/playererror/PlayerErrorHandler.java
@@ -0,0 +1,89 @@
+package org.schabi.newpipe.player.playererror;
+
+import android.content.Context;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.preference.PreferenceManager;
+
+import com.google.android.exoplayer2.ExoPlaybackException;
+
+import org.schabi.newpipe.R;
+import org.schabi.newpipe.error.EnsureExceptionSerializable;
+import org.schabi.newpipe.error.ErrorActivity;
+import org.schabi.newpipe.error.ErrorInfo;
+import org.schabi.newpipe.error.UserAction;
+import org.schabi.newpipe.extractor.Info;
+
+/**
+ * Handles (exoplayer)errors that occur in the player.
+ */
+public class PlayerErrorHandler {
+ // This has to be <= 23 chars on devices running Android 7 or lower (API <= 25)
+ // or it fails with an IllegalArgumentException
+ // https://stackoverflow.com/a/54744028
+ private static final String TAG = "PlayerErrorHandler";
+
+ @Nullable
+ private Toast errorToast;
+
+ @NonNull
+ private final Context context;
+
+ public PlayerErrorHandler(@NonNull final Context context) {
+ this.context = context;
+ }
+
+ public void showPlayerError(
+ @NonNull final ExoPlaybackException exception,
+ @NonNull final Info info,
+ @StringRes final int textResId
+ ) {
+ // Hide existing toast message
+ if (errorToast != null) {
+ Log.d(TAG, "Trying to cancel previous player error error toast");
+ errorToast.cancel();
+ errorToast = null;
+ }
+
+ if (shouldReportError()) {
+ try {
+ reportError(exception, info);
+ // When a report pops up we need no toast
+ return;
+ } catch (final Exception ex) {
+ Log.w(TAG, "Unable to report error:", ex);
+ // This will show the toast as fallback
+ }
+ }
+
+ Log.d(TAG, "Showing player error toast");
+ errorToast = Toast.makeText(context, textResId, Toast.LENGTH_SHORT);
+ errorToast.show();
+ }
+
+ private void reportError(@NonNull final ExoPlaybackException exception,
+ @NonNull final Info info) {
+ ErrorActivity.reportError(
+ context,
+ new ErrorInfo(
+ EnsureExceptionSerializable.ensureSerializable(exception),
+ UserAction.PLAY_STREAM,
+ "Player error[type=" + exception.type + "] occurred while playing: "
+ + info.getUrl(),
+ info
+ )
+ );
+ }
+
+ private boolean shouldReportError() {
+ return PreferenceManager
+ .getDefaultSharedPreferences(context)
+ .getBoolean(
+ context.getString(R.string.report_player_errors_key),
+ false);
+ }
+}
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 014c1333901..f2259b1202c 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
@@ -436,14 +436,16 @@ public synchronized void unsetRecovery(final int index) {
* top, so shuffling a size-2 list does nothing)
*/
public synchronized void shuffle() {
- // Can't shuffle an list that's empty or only has one element
- if (size() <= 2) {
- return;
- }
// Create a backup if it doesn't already exist
+ // Note: The backup-list has to be created at all cost (even when size <= 2).
+ // Otherwise it's not possible to enter shuffle-mode!
if (backup == null) {
backup = new ArrayList<>(streams);
}
+ // Can't shuffle a list that's empty or only has one element
+ if (size() <= 2) {
+ return;
+ }
final int originalIndex = getIndex();
final PlayQueueItem currentItem = getItem();
diff --git a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
index 5fee436596e..b283e105ec6 100644
--- a/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
+++ b/app/src/main/java/org/schabi/newpipe/player/playqueue/PlayQueueItemTouchCallback.java
@@ -51,6 +51,6 @@ public boolean isItemViewSwipeEnabled() {
@Override
public void onSwiped(final RecyclerView.ViewHolder viewHolder, final int swipeDir) {
- onSwiped(viewHolder.getAdapterPosition());
+ onSwiped(viewHolder.getBindingAdapterPosition());
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
index 81e629c2f30..cfe9dbb62b2 100644
--- a/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
+++ b/app/src/main/java/org/schabi/newpipe/player/resolver/PlaybackResolver.java
@@ -9,6 +9,7 @@
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.source.MediaSource;
+import com.google.android.exoplayer2.source.MediaSourceFactory;
import com.google.android.exoplayer2.util.Util;
import org.schabi.newpipe.extractor.stream.StreamInfo;
@@ -41,20 +42,28 @@ default MediaSource buildLiveMediaSource(@NonNull final PlayerDataSource dataSou
@NonNull final String sourceUrl,
@C.ContentType final int type,
@NonNull final MediaSourceTag metadata) {
- final Uri uri = Uri.parse(sourceUrl);
+ final MediaSourceFactory factory;
switch (type) {
case C.TYPE_SS:
- return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveSsMediaSourceFactory();
+ break;
case C.TYPE_DASH:
- return dataSource.getLiveDashMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveDashMediaSourceFactory();
+ break;
case C.TYPE_HLS:
- return dataSource.getLiveHlsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveHlsMediaSourceFactory();
+ break;
default:
throw new IllegalStateException("Unsupported type: " + type);
}
+
+ return factory.createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(Uri.parse(sourceUrl))
+ .setLiveTargetOffsetMs(PlayerDataSource.LIVE_STREAM_EDGE_GAP_MILLIS)
+ .build()
+ );
}
@NonNull
@@ -67,21 +76,30 @@ default MediaSource buildMediaSource(@NonNull final PlayerDataSource dataSource,
@C.ContentType final int type = TextUtils.isEmpty(overrideExtension)
? Util.inferContentType(uri) : Util.inferContentType("." + overrideExtension);
+ final MediaSourceFactory factory;
switch (type) {
case C.TYPE_SS:
- return dataSource.getLiveSsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getLiveSsMediaSourceFactory();
+ break;
case C.TYPE_DASH:
- return dataSource.getDashMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getDashMediaSourceFactory();
+ break;
case C.TYPE_HLS:
- return dataSource.getHlsMediaSourceFactory().setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getHlsMediaSourceFactory();
+ break;
case C.TYPE_OTHER:
- return dataSource.getExtractorMediaSourceFactory(cacheKey).setTag(metadata)
- .createMediaSource(MediaItem.fromUri(uri));
+ factory = dataSource.getExtractorMediaSourceFactory();
+ break;
default:
throw new IllegalStateException("Unsupported type: " + type);
}
+
+ return factory.createMediaSource(
+ new MediaItem.Builder()
+ .setTag(metadata)
+ .setUri(uri)
+ .setCustomCacheKey(cacheKey)
+ .build()
+ );
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt
index 6bc63a56aa9..3ac2756952e 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt
+++ b/app/src/main/java/org/schabi/newpipe/settings/ContentSettingsManager.kt
@@ -1,6 +1,7 @@
package org.schabi.newpipe.settings
import android.content.SharedPreferences
+import android.util.Log
import org.schabi.newpipe.streams.io.SharpOutputStream
import org.schabi.newpipe.streams.io.StoredFileHelper
import org.schabi.newpipe.util.ZipHelper
@@ -13,6 +14,9 @@ import java.io.ObjectOutputStream
import java.util.zip.ZipOutputStream
class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
+ companion object {
+ const val TAG = "ContentSetManager"
+ }
/**
* Exports given [SharedPreferences] to the file in given outputPath.
@@ -31,7 +35,7 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
output.flush()
}
} catch (e: IOException) {
- e.printStackTrace()
+ Log.e(TAG, "Unable to exportDatabase", e)
}
ZipHelper.addFileToZip(outZip, fileLocator.settings.path, "newpipe.settings")
@@ -101,9 +105,9 @@ class ContentSettingsManager(private val fileLocator: NewPipeFileLocator) {
preferenceEditor.commit()
}
} catch (e: IOException) {
- e.printStackTrace()
+ Log.e(TAG, "Unable to loadSharedPreferences", e)
} catch (e: ClassNotFoundException) {
- e.printStackTrace()
+ Log.e(TAG, "Unable to loadSharedPreferences", e)
}
}
}
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 2f65af4d6b0..12599b828b4 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/MainSettingsFragment.java
@@ -16,8 +16,9 @@ public class MainSettingsFragment extends BasePreferenceFragment {
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.main_settings);
- if (!CheckForNewAppVersion.isGithubApk(App.getApp())) {
- final Preference update = findPreference(getString(R.string.update_pref_screen_key));
+ if (!CheckForNewAppVersion.isReleaseApk(App.getApp())) {
+ final Preference update
+ = findPreference(getString(R.string.update_pref_screen_key));
getPreferenceScreen().removePreference(update);
defaultPreferences.edit().putBoolean(getString(R.string.update_app_key), false).apply();
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 5f388efb75d..dfc053a6257 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/PeertubeInstanceListFragment.java
@@ -303,8 +303,8 @@ public boolean onMove(@NonNull final RecyclerView recyclerView,
return false;
}
- final int sourceIndex = source.getAdapterPosition();
- final int targetIndex = target.getAdapterPosition();
+ final int sourceIndex = source.getBindingAdapterPosition();
+ final int targetIndex = target.getBindingAdapterPosition();
instanceListAdapter.swapItems(sourceIndex, targetIndex);
return true;
}
@@ -322,7 +322,7 @@ public boolean isItemViewSwipeEnabled() {
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
- final int position = viewHolder.getAdapterPosition();
+ final int position = viewHolder.getBindingAdapterPosition();
// do not allow swiping the selected instance
if (instanceList.get(position).getUrl().equals(selectedInstance.getUrl())) {
instanceListAdapter.notifyItemChanged(position);
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 d2f56b4874b..bc183d08a6a 100644
--- a/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
+++ b/app/src/main/java/org/schabi/newpipe/settings/UpdateSettingsFragment.java
@@ -1,34 +1,48 @@
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.R;
-import static org.schabi.newpipe.CheckForNewAppVersion.startNewVersionCheckService;
-
public class UpdateSettingsFragment extends BasePreferenceFragment {
private final Preference.OnPreferenceChangeListener updatePreferenceChange
= (preference, checkForUpdates) -> {
defaultPreferences.edit()
.putBoolean(getString(R.string.update_app_key), (boolean) checkForUpdates).apply();
- if ((boolean) checkForUpdates) {
- // Search for updates immediately when update checks are enabled.
- // 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();
- }
+ if ((boolean) checkForUpdates) {
+ checkNewVersionNow();
+ }
+ return true;
+ };
+
+ private final Preference.OnPreferenceClickListener manualUpdateClick
+ = preference -> {
+ Toast.makeText(getContext(), R.string.checking_updates_toast, Toast.LENGTH_SHORT).show();
+ checkNewVersionNow();
return true;
};
+ private void checkNewVersionNow() {
+ // Search for updates immediately when update checks are enabled.
+ // 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();
+ }
+
@Override
public void onCreatePreferences(final Bundle savedInstanceState, final String rootKey) {
addPreferencesFromResource(R.xml.update_settings);
- final String updateToggleKey = getString(R.string.update_app_key);
- findPreference(updateToggleKey).setOnPreferenceChangeListener(updatePreferenceChange);
+ findPreference(getString(R.string.update_app_key))
+ .setOnPreferenceChangeListener(updatePreferenceChange);
+ findPreference(getString(R.string.manual_update_key))
+ .setOnPreferenceClickListener(manualUpdateClick);
}
}
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 6e50765ba8d..c9eb42fca17 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
@@ -299,8 +299,8 @@ public boolean onMove(@NonNull final RecyclerView recyclerView,
return false;
}
- final int sourceIndex = source.getAdapterPosition();
- final int targetIndex = target.getAdapterPosition();
+ final int sourceIndex = source.getBindingAdapterPosition();
+ final int targetIndex = target.getBindingAdapterPosition();
selectedTabsAdapter.swapItems(sourceIndex, targetIndex);
return true;
}
@@ -318,7 +318,7 @@ public boolean isItemViewSwipeEnabled() {
@Override
public void onSwiped(@NonNull final RecyclerView.ViewHolder viewHolder,
final int swipeDir) {
- final int position = viewHolder.getAdapterPosition();
+ final int position = viewHolder.getBindingAdapterPosition();
tabList.remove(position);
selectedTabsAdapter.notifyItemRemoved(position);
diff --git a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
index bdf5e8ce4ce..bbe9a7edb29 100644
--- a/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
+++ b/app/src/main/java/org/schabi/newpipe/util/DeviceUtils.java
@@ -6,6 +6,7 @@
import android.content.res.Configuration;
import android.os.BatteryManager;
import android.os.Build;
+import android.provider.Settings;
import android.util.TypedValue;
import android.view.KeyEvent;
@@ -144,4 +145,11 @@ public static boolean isLandscape(final Context context) {
public static boolean isInMultiWindow(final AppCompatActivity activity) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity.isInMultiWindowMode();
}
+
+ public static boolean hasAnimationsAnimatorDurationEnabled(final Context context) {
+ return Settings.System.getFloat(
+ context.getContentResolver(),
+ Settings.Global.ANIMATOR_DURATION_SCALE,
+ 1F) != 0F;
+ }
}
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 cb796a7a685..c01e051b023 100644
--- a/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
@@ -157,7 +157,9 @@ public static void playOnPopupPlayer(final Context context,
return;
}
- Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
+ if (PlayerHolder.getInstance().getType() != PlayerType.POPUP) {
+ Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();
+ }
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.POPUP.ordinal());
ContextCompat.startForegroundService(context, intent);
@@ -166,8 +168,10 @@ public static void playOnPopupPlayer(final Context context,
public static void playOnBackgroundPlayer(final Context context,
final PlayQueue queue,
final boolean resumePlayback) {
- Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
- .show();
+ if (PlayerHolder.getInstance().getType() != MainPlayer.PlayerType.AUDIO) {
+ Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
+ .show();
+ }
final Intent intent = getPlayerIntent(context, MainPlayer.class, queue, resumePlayback);
intent.putExtra(Player.PLAYER_TYPE, MainPlayer.PlayerType.AUDIO.ordinal());
ContextCompat.startForegroundService(context, intent);
@@ -502,6 +506,27 @@ public static void openVideoDetail(final Context context,
context.startActivity(intent);
}
+ /**
+ * Opens {@link ChannelFragment}.
+ * Use this instead of {@link #openChannelFragment(FragmentManager, int, String, String)}
+ * when no fragments are used / no FragmentManager is available.
+ * @param context
+ * @param serviceId
+ * @param url
+ * @param title
+ */
+ public static void openChannelFragmentUsingIntent(final Context context,
+ final int serviceId,
+ final String url,
+ @NonNull final String title) {
+ final Intent intent = getOpenIntent(context, url, serviceId,
+ StreamingService.LinkType.CHANNEL);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(Constants.KEY_TITLE, title);
+
+ context.startActivity(intent);
+ }
+
public static void openMainActivity(final Context context) {
final Intent mIntent = new Intent(context, MainActivity.class);
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
diff --git a/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java b/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java
new file mode 100644
index 00000000000..cf1a9a03ad9
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/util/NewPipeTextViewHelper.java
@@ -0,0 +1,61 @@
+package org.schabi.newpipe.util;
+
+import android.content.Context;
+import android.text.Selection;
+import android.text.Spannable;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+import org.schabi.newpipe.views.NewPipeEditText;
+import org.schabi.newpipe.views.NewPipeTextView;
+
+public final class NewPipeTextViewHelper {
+ private NewPipeTextViewHelper() {
+ }
+
+ /**
+ * Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
+ * {@link NewPipeEditText NewPipeEditTexts} with
+ * {@link ShareUtils#shareText(Context, String, String)}.
+ *
+ *
+ * This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
+ * using the {@code Share} command of the popup menu which appears when selecting text.
+ *
+ *
+ * @param textView the {@link TextView} on which sharing the selected text. It should be a
+ * {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
+ * {@link TextView standard TextViews} are supported).
+ */
+ public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
+ final CharSequence textViewText = textView.getText();
+ shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
+ if (textViewText instanceof Spannable) {
+ Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
+ }
+ }
+
+ @Nullable
+ private static CharSequence getSelectedText(@NonNull final TextView textView,
+ @Nullable final CharSequence text) {
+ if (!textView.hasSelection() || text == null) {
+ return null;
+ }
+
+ final int start = textView.getSelectionStart();
+ final int end = textView.getSelectionEnd();
+ return String.valueOf(start > end ? text.subSequence(end, start)
+ : text.subSequence(start, end));
+ }
+
+ private static void shareSelectedTextIfNotNullAndNotEmpty(
+ @NonNull final TextView textView,
+ @Nullable final CharSequence selectedText) {
+ if (selectedText != null && selectedText.length() != 0) {
+ ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
+ }
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
index ec51cc37013..0ffbe4137bf 100644
--- a/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
+++ b/app/src/main/java/org/schabi/newpipe/util/StreamDialogEntry.java
@@ -5,12 +5,15 @@
import android.widget.Toast;
import androidx.fragment.app.Fragment;
+import androidx.preference.PreferenceManager;
import org.schabi.newpipe.NewPipeDatabase;
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.PlaylistCreationDialog;
+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;
@@ -81,14 +84,16 @@ public enum StreamDialogEntry {
delete(R.string.delete, (fragment, item) -> {
}), // has to be set manually
- append_playlist(R.string.append_playlist, (fragment, item) -> {
- final PlaylistAppendDialog d = PlaylistAppendDialog
- .fromStreamInfoItems(Collections.singletonList(item));
-
- PlaylistAppendDialog.onPlaylistFound(fragment.getContext(),
- () -> d.show(fragment.getParentFragmentManager(), "StreamDialogEntry@append_playlist"),
- () -> PlaylistCreationDialog.newInstance(d)
- .show(fragment.getParentFragmentManager(), "StreamDialogEntry@create_playlist")
+ 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"
+ )
);
}),
@@ -191,6 +196,16 @@ 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 //
/////////////////////////////////////////////
diff --git a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java
index 8cf5a4cdf83..05e69408a9d 100644
--- a/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java
+++ b/app/src/main/java/org/schabi/newpipe/util/TLSSocketFactoryCompat.java
@@ -10,9 +10,8 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManager;
-import static org.schabi.newpipe.MainActivity.DEBUG;
+import android.util.Log;
/**
@@ -21,6 +20,7 @@
*/
public class TLSSocketFactoryCompat extends SSLSocketFactory {
+ private static final String TAG = "TLSSocketFactoryCom";
private static TLSSocketFactoryCompat instance = null;
@@ -32,14 +32,6 @@ public TLSSocketFactoryCompat() throws KeyManagementException, NoSuchAlgorithmEx
internalSSLSocketFactory = context.getSocketFactory();
}
-
- public TLSSocketFactoryCompat(final TrustManager[] tm)
- throws KeyManagementException, NoSuchAlgorithmException {
- final SSLContext context = SSLContext.getInstance("TLS");
- context.init(null, tm, new java.security.SecureRandom());
- internalSSLSocketFactory = context.getSocketFactory();
- }
-
public static TLSSocketFactoryCompat getInstance()
throws NoSuchAlgorithmException, KeyManagementException {
if (instance != null) {
@@ -53,9 +45,7 @@ public static void setAsDefault() {
try {
HttpsURLConnection.setDefaultSSLSocketFactory(getInstance());
} catch (NoSuchAlgorithmException | KeyManagementException e) {
- if (DEBUG) {
- e.printStackTrace();
- }
+ Log.e(TAG, "Unable to setAsDefault", e);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java
new file mode 100644
index 00000000000..2adc28d0e5e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeEditText.java
@@ -0,0 +1,45 @@
+package org.schabi.newpipe.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatEditText;
+
+import org.schabi.newpipe.util.NewPipeTextViewHelper;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+/**
+ * An {@link AppCompatEditText} which uses {@link ShareUtils#shareText(Context, String, String)}
+ * when sharing selected text by using the {@code Share} command of the floating actions.
+ *
+ * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text
+ * from {@link AppCompatEditText} on EMUI devices.
+ *
+ */
+public class NewPipeEditText extends AppCompatEditText {
+
+ public NewPipeEditText(@NonNull final Context context) {
+ super(context);
+ }
+
+ public NewPipeEditText(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NewPipeEditText(@NonNull final Context context,
+ @Nullable final AttributeSet attrs,
+ final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public boolean onTextContextMenuItem(final int id) {
+ if (id == android.R.id.shareText) {
+ NewPipeTextViewHelper.shareSelectedTextWithShareUtils(this);
+ return true;
+ }
+ return super.onTextContextMenuItem(id);
+ }
+}
diff --git a/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java
new file mode 100644
index 00000000000..8fdac32db7e
--- /dev/null
+++ b/app/src/main/java/org/schabi/newpipe/views/NewPipeTextView.java
@@ -0,0 +1,45 @@
+package org.schabi.newpipe.views;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+
+import org.schabi.newpipe.util.NewPipeTextViewHelper;
+import org.schabi.newpipe.util.external_communication.ShareUtils;
+
+/**
+ * An {@link AppCompatTextView} which uses {@link ShareUtils#shareText(Context, String, String)}
+ * when sharing selected text by using the {@code Share} command of the floating actions.
+ *
+ * This allows NewPipe to show Android share sheet instead of EMUI share sheet when sharing text
+ * from {@link AppCompatTextView} on EMUI devices.
+ *
+ */
+public class NewPipeTextView extends AppCompatTextView {
+
+ public NewPipeTextView(@NonNull final Context context) {
+ super(context);
+ }
+
+ public NewPipeTextView(@NonNull final Context context, @Nullable final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NewPipeTextView(@NonNull final Context context,
+ @Nullable final AttributeSet attrs,
+ final int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ public boolean onTextContextMenuItem(final int id) {
+ if (id == android.R.id.shareText) {
+ NewPipeTextViewHelper.shareSelectedTextWithShareUtils(this);
+ return true;
+ }
+ return super.onTextContextMenuItem(id);
+ }
+}
diff --git a/app/src/main/res/layout-land/activity_player_queue_control.xml b/app/src/main/res/layout-land/activity_player_queue_control.xml
index 4b79d92f60a..c2359552ecd 100644
--- a/app/src/main/res/layout-land/activity_player_queue_control.xml
+++ b/app/src/main/res/layout-land/activity_player_queue_control.xml
@@ -60,7 +60,7 @@
android:padding="8dp"
tools:ignore="RtlHardcoded,RtlSymmetry">
-
-
-
-
-
-
-
-
-
-
+ tools:visibility="gone" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -82,7 +82,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:paddingStart="6dp"
+ android:paddingTop="4dp"
+ android:paddingEnd="6dp">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:orientation="vertical">
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_playlists.xml b/app/src/main/res/layout/dialog_playlists.xml
index b64a9241c14..99c46e55264 100644
--- a/app/src/main/res/layout/dialog_playlists.xml
+++ b/app/src/main/res/layout/dialog_playlists.xml
@@ -23,7 +23,7 @@
app:srcCompat="@drawable/ic_playlist_add"
tools:ignore="ContentDescription,RtlHardcoded" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -85,14 +85,14 @@
android:layout_gravity="end"
android:text="@string/give_back" />
-
-
@@ -105,14 +105,14 @@
android:layout_gravity="end"
android:text="@string/open_in_browser" />
-
-
diff --git a/app/src/main/res/layout/fragment_channel.xml b/app/src/main/res/layout/fragment_channel.xml
index 873f3c88489..9e22575391d 100644
--- a/app/src/main/res/layout/fragment_channel.xml
+++ b/app/src/main/res/layout/fragment_channel.xml
@@ -30,7 +30,7 @@
android:visibility="gone"
tools:visibility="visible">
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ tools:visibility="gone" />
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
- -->
-
-
-
- -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ tools:listitem="@layout/select_channel_item" />
-
-
-
+ tools:listitem="@layout/select_kiosk_item" />
diff --git a/app/src/main/res/layout/select_kiosk_item.xml b/app/src/main/res/layout/select_kiosk_item.xml
index 6cd04ae345a..680767bba0c 100644
--- a/app/src/main/res/layout/select_kiosk_item.xml
+++ b/app/src/main/res/layout/select_kiosk_item.xml
@@ -22,7 +22,7 @@
app:tint="@color/contrastColor"
tools:ignore="RtlHardcoded" />
-
-
-
-
diff --git a/app/src/main/res/layout/settings_category_header_title.xml b/app/src/main/res/layout/settings_category_header_title.xml
index 679b9048c3e..c7d6920b049 100644
--- a/app/src/main/res/layout/settings_category_header_title.xml
+++ b/app/src/main/res/layout/settings_category_header_title.xml
@@ -1,5 +1,5 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/menu/menu_play_queue_item.xml b/app/src/main/res/menu/menu_play_queue_item.xml
index ebb361be971..b23f8008f72 100644
--- a/app/src/main/res/menu/menu_play_queue_item.xml
+++ b/app/src/main/res/menu/menu_play_queue_item.xml
@@ -9,7 +9,10 @@
android:title="@string/play_queue_stream_detail" />
+ android:title="@string/add_to_playlist" />
+
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index f90a82c44fe..55f41563c34 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -235,7 +235,7 @@
إنشاء قائمة تشغيل جديدة
إعادة تسمية
التسمية
- إضافة إلى قائمة التشغيل
+ إضافة إلى قائمة التشغيل
هل تريد حذف قائمة التشغيل هذه؟
تم إنشاء قائمة التشغيل
تمت إضافتها إلى قائمة التشغيل
diff --git a/app/src/main/res/values-b+ast/strings.xml b/app/src/main/res/values-b+ast/strings.xml
index 213ef6f482a..19ed896f0a6 100644
--- a/app/src/main/res/values-b+ast/strings.xml
+++ b/app/src/main/res/values-b+ast/strings.xml
@@ -132,7 +132,7 @@
Escartar
Llista de reproducción nueva
Nome
- Amestar a una llista de reproducción
+ Amestar a una llista de reproducción
¿Desanicair esta llista de reproducción\?
Ensin sotítulos
Axustar
diff --git a/app/src/main/res/values-b+uz+Latn/strings.xml b/app/src/main/res/values-b+uz+Latn/strings.xml
index 16f28dc09f9..4d4602daa16 100644
--- a/app/src/main/res/values-b+uz+Latn/strings.xml
+++ b/app/src/main/res/values-b+uz+Latn/strings.xml
@@ -534,7 +534,7 @@
Pleylist eskizi sifatida o\'rnating
Unmute rejimi
Mute rejimi
- Playlist qo\'shish
+ Playlist qo\'shish
Nomi
Nomni o\'zgartirish
Yangi Playlist
diff --git a/app/src/main/res/values-b+zh+HANS+CN/strings.xml b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
index 9670c318d76..20b01f60518 100644
--- a/app/src/main/res/values-b+zh+HANS+CN/strings.xml
+++ b/app/src/main/res/values-b+zh+HANS+CN/strings.xml
@@ -259,7 +259,7 @@
新建播放列表
重命名
名称
- 添加至播放列表
+ 添加至播放列表
设为播放列表封面
收藏播放列表
删除收藏
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 46eedac346e..4b185b30149 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -274,7 +274,7 @@
Стварыць плэйліст
Перайменаваць
Імя
- Дадаць у плэйліст
+ Дадаць у плэйліст
На мініяцюру плэйліста
Дадаць плэйліст у закладкі
Выдаліць закладку
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index f1edfa1247b..1c16e225be9 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -267,7 +267,7 @@
Нов Плейлист
Преименувай
Име
- Добави Към Плейлист
+ Добави Към Плейлист
Задай като миниатюра на плейлиста
Миниатюрата на плейлиста е сменена
Премахни отметката
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index 6198b926ee7..d561d52e846 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -188,7 +188,7 @@
বুকমার্ক মুছুন
আন মিউট
মিউট
- প্লে লিস্ট এ যুক্ত করুন
+ প্লে লিস্ট এ যুক্ত করুন
নাম
নতুন প্লে লিস্ট
সবসময় জিজ্ঞেস করুন
diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml
index 79ce1caaa5c..8313640139c 100644
--- a/app/src/main/res/values-bn-rIN/strings.xml
+++ b/app/src/main/res/values-bn-rIN/strings.xml
@@ -167,7 +167,7 @@
প্লে লিস্ট ডিলিট করতে চান\?
আন মিউট
মিউট
- প্লে লিস্ট এ যুক্ত করুন
+ প্লে লিস্ট এ যুক্ত করুন
সবসময় জিজ্ঞেস করুন
ভিডিও প্লেয়ার
ড্রয়ার বন্ধ করুন
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index a2f9dec312d..11225e7063e 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -37,7 +37,7 @@
বুকমার্ক মুছুন
আন মিউট
মিউট
- প্লে লিস্ট এ যুক্ত করুন
+ প্লে লিস্ট এ যুক্ত করুন
নাম
নাম পাল্টাও
নতুন প্লে লিস্ট
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index cbd725c774c..5e1d1b2015e 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -86,7 +86,7 @@
Crea una llista de reproducció
Canvia el nom
Nom
- Afegeix a una llista de reproducció
+ Afegeix a una llista de reproducció
Importa
Importa des de
Exporta a
diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml
index b04048c5fc3..65af5ffff71 100644
--- a/app/src/main/res/values-ckb/strings.xml
+++ b/app/src/main/res/values-ckb/strings.xml
@@ -403,7 +403,7 @@
ناوفایل
دانان لەسەر وێنۆچکەی خشتەلێدان
دەربارەی نیوپایپ
- زیادکردن بۆ خشتەلێدان
+ زیادکردن بۆ خشتەلێدان
(نەزانراو)
زمانی بهرنامه
پەڕەی کیۆسک
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 1791c179457..5a4091db513 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -234,7 +234,7 @@
Nový playlist
Přejmenovat
Jméno
- Přidat do playlistu
+ Přidat do playlistu
Nastavit jako miniaturu playlistu
Přidat playlist do záložek
Smazat záložku
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 158ec44ec99..d59c35edb02 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -270,7 +270,7 @@
Ny spilleliste
Omdøb
Navn
- Føj til spilleliste
+ Føj til spilleliste
Slet denne spilleliste\?
Spilleliste oprettet
Ingen undertekster
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index ca4d3e4d96a..105646a167e 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -228,7 +228,7 @@
Immer fragen
Neue Wiedergabeliste
Umbenennen
- Zur Wiedergabeliste hinzufügen
+ Zur Wiedergabeliste hinzufügen
Als Vorschaubild der Wiedergabeliste festlegen
Lesezeichen entfernen
Diese Wiedergabeliste löschen?
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 3d4343195d6..1775e1ca830 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -270,7 +270,7 @@
Νέα λίστα αναπαραγωγής
Μετονομασία
Όνομα
- Προσθήκη σε λίστα αναπαραγωγής
+ Προσθήκη σε λίστα αναπαραγωγής
Ορισμός ως μικρογραφία λίστας αναπαραγωγής
Προσθήκη σελιδοδείκτη στη λίστα
Διαγραφή σελιδοδείκτη
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index 50cdf09b323..76927234574 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -153,7 +153,7 @@
Nova ludlisto
Alinomi
Nomo
- Aldoni al la ludlisto
+ Aldoni al la ludlisto
Meti kiel bildeto de ludlisto
Legosigno Ludlisto
Forviŝi Legosignon
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 1e0d406a103..0c8ba0a9e70 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -231,7 +231,7 @@
Lista de reproducción nueva
Cambiar nombre
Nombre
- Añadir a la lista de reproducción
+ Añadir a la lista de reproducción
Definir como miniatura de lista de reproducción
Marcar lista de reproducción
Quitar marcador
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
index 1321806f59b..107c0f624f4 100644
--- a/app/src/main/res/values-et/strings.xml
+++ b/app/src/main/res/values-et/strings.xml
@@ -256,7 +256,7 @@
Uus esitusloend
Nimeta ümber
Nimi
- Lisa esitusloendisse
+ Lisa esitusloendisse
Määra esitusloendi pisipildiks
Lisa esitusloend järjehoidjaks
Eemalda järjehoidja
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 9ca4bfae67e..5b8161bf241 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -259,7 +259,7 @@
Erreprodukzio-zerrenda berria
Aldatu izena
Izena
- Gehitu erreprodukzio-zerrendara
+ Gehitu erreprodukzio-zerrendara
Ezarri erreprodukzio-zerrendaren iruditxo gisa
Gogoko erreprodukzio-zerrenda
Kendu gogokoa
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index e66825b9648..58f6cef814c 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -217,7 +217,7 @@
فهرست پخش جدید
تغییر نام
نام
- افزودن به سیاههٔ پخش
+ افزودن به سیاههٔ پخش
تنظیم برای تصویر سیاههٔ پخش
این فهرست پخش پاک شود؟
فهرست پخش ایجاد شد
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 25ae94dd515..cd6777b5e19 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -271,7 +271,7 @@
Uusi soittolista
Uudelleennimeä
Nimi
- Lisää soittolistaan
+ Lisää soittolistaan
Aseta soittolistan kuvakkeeksi
Tallenna soittolista kirjanmerkkeihin
Poista kirjanmerkki
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index f4226497a94..8d27ddff986 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -230,7 +230,7 @@
Nouvelle liste de lecture
Renommer
Nom
- Ajouter à la liste de lecture
+ Ajouter à la liste de lecture
Définir comme miniature de la liste de lecture
Enregister la liste de lecture
Supprimer le signet
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 185fdda9aa2..f22ecff8603 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -270,7 +270,7 @@
Nova lista de reprodución
Renomear
Nome
- Engadir á lista de reprodución
+ Engadir á lista de reprodución
Estabelecer como miniatura da lista de reprodución
Gardar a lista de reprodución nos marcadores
Eliminar o marcador
diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml
index 6bc06eb3e36..1d054155a23 100644
--- a/app/src/main/res/values-he/strings.xml
+++ b/app/src/main/res/values-he/strings.xml
@@ -240,7 +240,7 @@
רשימת נגינה חדשה
שינוי שם
שם
- הוספה לרשימת נגינה
+ הוספה לרשימת נגינה
הוספת רשימת נגינה לסימניות
הסרת סימנייה
למחוק רשימת נגינה זו\?
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index 6402fc63814..5f295bda8eb 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -230,7 +230,7 @@
नई प्लेलिस्ट
नाम बदलें
नाम
- प्लेलिस्ट में जोड़ें
+ प्लेलिस्ट में जोड़ें
प्लेलिस्ट थंबनेल के रूप में सेट करें
प्लेलिस्ट बुकमार्क करें
बुकमार्क हटायें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 88e79451e19..1f8ef42a001 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -202,7 +202,7 @@
Nova playlista
Preimenuj
Ime
- Dodaj u playlistu
+ Dodaj u playlistu
Postavi kao minijaturu playliste
Zabilježi playlistu
Ukloni zabilješku
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index ca1913403ad..e19fade838f 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -254,7 +254,7 @@
Új lejátszási lista
Átnevezés
Név
- Lejátszási listához adás
+ Lejátszási listához adás
Beállítás lejátszási lista indexképeként
Könyvjelző törlése
Törlődjön ez a lejátszási lista\?
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 82c867f8608..dddf0dd6a18 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -229,7 +229,7 @@
Daftar Putar Baru
Ubah Nama
Nama
- Tambah ke daftar putar
+ Tambah ke daftar putar
Atur sebagai thumbnail daftar putar
Markah Daftar Putar
Hapus Markah
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index af9d529d3cf..2ee99d45288 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -231,7 +231,7 @@
Nuova playlist
Rinomina
Nome
- Aggiungi alla playlist
+ Aggiungi alla playlist
Imposta come copertina della playlist
Salva playlist
Rimuovi playlist
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 769a31b5205..f2540f20505 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -221,7 +221,7 @@
新規プレイリスト
変更
プレイリスト名
- プレイリストに追加
+ プレイリストに追加
プレイリストのサムネイルに設定
プレイリストをブックマーク
ブックマークを削除
diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml
index 3fa5f2f45dd..d7ac85dd126 100644
--- a/app/src/main/res/values-kmr/strings.xml
+++ b/app/src/main/res/values-kmr/strings.xml
@@ -498,7 +498,7 @@
Wekî Thumbnailê Lîsteya Lîsteyê saz bikin
Bê deng kirin
Bêdeng
- Li lîsteyê zêde bike
+ Li lîsteyê zêde bike
Nav
Navlêkirin
Lîstinê ya nû
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index aaf02a5dd17..880c45aee24 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -230,7 +230,7 @@
새로운 재생목록
이름 바꾸기
이름
- 재생목록에 추가
+ 재생목록에 추가
재생목록 썸네일로 설정
재생목록 북마크하기
북마크 제거하기
diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml
index 2cdd31d3b82..be2a8086f78 100644
--- a/app/src/main/res/values-ku/strings.xml
+++ b/app/src/main/res/values-ku/strings.xml
@@ -212,7 +212,7 @@
لیستەلێدانی نوێ
ناولێنانەوە
ناو
- زیادکردن بۆ لیستەلێدان
+ زیادکردن بۆ لیستەلێدان
دانان لەسەر وێنۆچکەی لیستەلێدان
لیستەلێدانی نیشانەکراو
لادانی نیشانەکراو
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index dae17e3ffcf..d0249568e18 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -235,7 +235,7 @@
Naujas grojaraštį
Pervadinti
Pavadinimas
- Pridėti į grojaraštį
+ Pridėti į grojaraštį
Nustatyti kaip grojaraščio miniatiūrą
Pridėti grojaraštį į žymes
Pašalinti žymes
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index 01167c42641..58d40b61d37 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -5,7 +5,7 @@
Atskaņošanas saraksts radīts
Dzēst atskaņošanas sarakstu\?
Iestatīt, kā atskaņošanas saraksta attēlu
- Pievienot atskaņošanas sarakstam
+ Pievienot atskaņošanas sarakstam
Vārds
Pārsaukt
Jauns Atskaņošanas Saraksts
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index c0e16def262..d925e29701f 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -248,7 +248,7 @@
Создај нова плејлиста
Прекрсти плејлиста
Име
- Додај во плејлиста
+ Додај во плејлиста
Постави како икона на плејлистата
Обележи ја плејлистата
Избриши ја белешката
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 20372cbb9ce..eb78ca44ce4 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -54,7 +54,7 @@
പ്ലേലിസ്റ്റ് ലഘുചിത്രമായി സജ്ജമാക്കുക
അൺമ്യൂട്ട്
മ്യൂട്ട്
- പ്ലേലിസ്റ്റിലേക്ക് ചേർക്കുക
+ പ്ലേലിസ്റ്റിലേക്ക് ചേർക്കുക
പേര്
പേര് മാറ്റുക
പുതിയ പ്ലേലിസ്റ്റ്
diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml
index ddf04c1e2eb..76aef646ff3 100644
--- a/app/src/main/res/values-ms/strings.xml
+++ b/app/src/main/res/values-ms/strings.xml
@@ -284,7 +284,7 @@
Senarai Main Baru
Namakan semula
Nama
- Tambah ke Senarai Main
+ Tambah ke Senarai Main
Tetapkan sebagai Thumbnail Senarai Main
Menanda senarai main
Hapuskan Penanda Halaman
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index b7c370900c5..9cce73360a6 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -231,7 +231,7 @@
Ny spilleliste
Gi nytt navn
Navn
- Legg til i spilleliste
+ Legg til i spilleliste
Sett som miniatyrbilde for spilleliste
Bokmerk spilleliste
Fjern bokmerke
diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml
index f26e8a05184..71c9e107fcf 100644
--- a/app/src/main/res/values-ne/strings.xml
+++ b/app/src/main/res/values-ne/strings.xml
@@ -292,7 +292,7 @@
नया प्लेलिस्ट
पुनः नामकरण
नाम
- प्लेसूचीमा थप्नुहोस
+ प्लेसूचीमा थप्नुहोस
प्लेलिस्ट थम्बनेल रूपमा सेट
बुकमार्क प्लेलिस्ट
बुकमार्क हटाउ
diff --git a/app/src/main/res/values-nl-rBE/strings.xml b/app/src/main/res/values-nl-rBE/strings.xml
index 5190e814e06..be762707993 100644
--- a/app/src/main/res/values-nl-rBE/strings.xml
+++ b/app/src/main/res/values-nl-rBE/strings.xml
@@ -252,7 +252,7 @@
Nieuwe afspeellijst
Hernoemen
Naam
- Toevoegen aan afspeellijst
+ Toevoegen aan afspeellijst
Instellen als miniatuur voor afspeellijst
Afspeellijst toevoegen aan bladwijzers
Bladwijzer verwijderen
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index f33e5d9cd1f..b835c68dcc2 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -231,7 +231,7 @@
Nieuwe afspeellijst
Naam wijzigen
Naam
- Toevoegen aan afspeellijst
+ Toevoegen aan afspeellijst
Instellen als miniatuur voor afspeellijst
Afspeellijst toevoegen aan bladwijzers
Bladwijzer verwijderen
diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml
index 75f40b72572..8fad329f38b 100644
--- a/app/src/main/res/values-pa/strings.xml
+++ b/app/src/main/res/values-pa/strings.xml
@@ -259,7 +259,7 @@
ਨਵੀਂ ਪਲੇ-ਲਿਸਟ
ਨਾਮ ਬਦਲੋ
ਨਾਮ
- ਪਲੇ-ਸੂਚੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ
+ ਪਲੇ-ਸੂਚੀ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰੋ
ਬਤੌਰ ਪਲੇ-ਸੂਚੀ ਥਮਨੇਲ ਸੈੱਟ ਕਰੋ
ਬੁੱਕਮਾਰਕ ਪਲੇ-ਲਿਸਟ
ਬੁੱਕਮਾਰਕ ਹਟਾਓ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 0de5abcf024..b4de246eb7d 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -238,7 +238,7 @@
Nowa playlista
Zmień nazwę
Nazwa
- Dodaj do playlisty
+ Dodaj do playlisty
Ustaw jako miniaturę playlisty
Dodaj do ulubionych
Usuń z ulubionych
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 1f53db3cebb..7c85328d0e6 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -229,7 +229,7 @@
Nova playlist
Renomear
Nome
- Adicionar à Playlist
+ Adicionar à Playlist
Definir Como Capa da Playlist
Favoritar playlist
Desfavoritar
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index ad4bb506190..4fa8e4e18b6 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -504,7 +504,7 @@
Utilizar pesquisa rápida
Está prestes a substituir a configuração atual.
Mudar para popup
- Adicionar à lista de reprodução
+ Adicionar à lista de reprodução
Reproduzir no modo poppup
Acha que a fonte demora muito tempo a carregar\? Se sim, tente ativar o carregamento rápido (pode alterar a opção nas definições ou no botão abaixo).
\n
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 7b8cd4cdc70..c1d95a797bf 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -242,7 +242,7 @@
A carregar o conteúdo solicitado
Nova lista de reprodução
Mudar nome
- Adicionar à lista de reprodução
+ Adicionar à lista de reprodução
Guardar lista de reprodução como favorita
Remover marcador
Eliminar esta lista de reprodução\?
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
index c1a8e784f12..19bd15385df 100644
--- a/app/src/main/res/values-ro/strings.xml
+++ b/app/src/main/res/values-ro/strings.xml
@@ -266,7 +266,7 @@
Ștergeți întregul istoric al vizionărilor\?
1 element șters.
Playlist nou
- Adăugați la playlist
+ Adăugați la playlist
Importare
Importați din
Exportați în
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index f895cea277f..16a42ef5b79 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -251,7 +251,7 @@
Новый плейлист
Переименовать
Имя
- В плейлист
+ В плейлист
На миниатюру плейлиста
Сохранить плейлист
Удалить плейлист
diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml
index 95ba064a5b4..1afb7990f90 100644
--- a/app/src/main/res/values-sc/strings.xml
+++ b/app/src/main/res/values-sc/strings.xml
@@ -39,7 +39,7 @@
Imposta che a miniadura de s\'iscalita
Allughe su sonu
Pone a sa muda
- Annanghe a s\'iscalita
+ Annanghe a s\'iscalita
Nùmene
Càmbia de nùmene
Iscalita noa
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 82655d4ad69..3df4ff0af60 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -234,7 +234,7 @@
Nový zoznam skladieb
Premenovať
Názov
- Pridať do zoznamu skladieb
+ Pridať do zoznamu skladieb
Nastaviť ako miniatúru zoznamu skladieb
Záložka zoznamu skladieb
Odstrániť Záložku
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index c6be5581b98..dbfef543d50 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -247,7 +247,7 @@
Napolni
Seznam predvajanja je bil ustvarjen
Izbrišem seznam predvajanja\?
- Dodaj v seznam predvajanja
+ Dodaj v seznam predvajanja
Preimenuj
Nov seznam predvajanja
Nalaganje zahtevano vsebino
diff --git a/app/src/main/res/values-so/strings.xml b/app/src/main/res/values-so/strings.xml
index 5455d728b52..bf9e7a768e5 100644
--- a/app/src/main/res/values-so/strings.xml
+++ b/app/src/main/res/values-so/strings.xml
@@ -115,7 +115,7 @@
Calaamadso Xulka
Ku fadhiisi galka xulka
Ka hadalsii
- Ku dar xul
+ Ku dar xul
Magaca ka baddal
Xul Cusub
Soo kicinaya shayga la codsaday
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index f9d2e8cfd91..10ab40e69bf 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -183,7 +183,7 @@
Vendose si pamjen statike të listës së luajtjes
Me zë
Pa zë
- Shto në listën e luajtjes
+ Shto në listën e luajtjes
Emri
Riemërto
Listë Luajtje e Re
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 628f2a10056..e27d9e8a849 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -350,7 +350,7 @@
Постави као сличицу листе
Упали звук
Утишај
- Додај на листу
+ Додај на листу
Назив
Преименуј
Нова листа
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 8eb7613296c..4fc9778bebc 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -271,7 +271,7 @@
Ny spellista
Byt namn
Namn
- Lägg till i spellista
+ Lägg till i spellista
Använd som spellistans miniatyrbild
Bokmärk spellistan
Ta bort bokmärke
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 22123e517d7..01946c321e7 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -270,7 +270,7 @@
เพลย์ลิสต์ใหม่
เปลี่ยนชื่อ
ชื่อ
- เพิ่มในเพลย์ลิสต์
+ เพิ่มในเพลย์ลิสต์
ตั้งเป็นรูปขนาดย่อของเพลย์ลิสต์
เก็บเพลย์ลิสต์
เอาที่คั่นหน้าออก
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index f802302ab15..f318de74484 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -227,7 +227,7 @@
Yeni Oynatma Listesi
Yeniden adlandır
Ad
- Oynatma listesine ekle
+ Oynatma listesine ekle
Oynatma listesi küçük resmi olarak ayarla
Oynatma listesini yer imlerine ekle
Yer imini kaldır
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index eae9079c005..c6b1f5bfde5 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -240,7 +240,7 @@
Нова добірка
Перейменувати
Назва
- Додати в добірку
+ Додати в добірку
Призначити ескізом добірки
Додати добірку до закладок
Видалити закладку
diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml
index 0454fc29321..1f427504b9e 100644
--- a/app/src/main/res/values-ur/strings.xml
+++ b/app/src/main/res/values-ur/strings.xml
@@ -248,7 +248,7 @@
نئی پلے لسٹ
نام تبدیل کریں
نام
- پلے لسٹ میں شامل
+ پلے لسٹ میں شامل
بطور پلے لسٹ انگشتی طے کریں
پلے لسٹ کو نشان زد کریں
نشان زد حدف کریں
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 83aea33c0a4..3635aa4b409 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -267,7 +267,7 @@
Tạo playlist mới
Đổi tên playlist
Tên
- Thêm vào danh sách phát
+ Thêm vào danh sách phát
Đặt làm hình thu nhỏ của danh sách phát
Đánh dấu playlist này
Xóa dấu trang
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index e5180c51e5c..f6318df0b68 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -259,7 +259,7 @@
新建播放列表
重 命名
名称
- 添加到播放列表
+ 添加到播放列表
设为播放列表缩略图
收藏播放列表
删除收藏
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 576f9d4807c..e5c499119ce 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -225,7 +225,7 @@
新的播放清單
重新命名
名稱
- 新增至播放清單
+ 新增至播放清單
設為播放清單縮圖
將播放清單加入書籤
移除書籤
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index 9261dfae166..e2b797576ab 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -89,6 +89,8 @@
- @string/never
+ report_player_errors_key
+
seekbar_preview_thumbnail_key
seekbar_preview_thumbnail_high_quality
seekbar_preview_thumbnail_low_quality
@@ -188,6 +190,7 @@
disable_media_tunneling_key
crash_the_app_key
show_image_indicators_key
+ show_crash_the_player_key
theme
@@ -263,6 +266,7 @@
feed_update_threshold_key
300
+ feed_show_played_items
show_thumbnail_key
@@ -359,6 +363,7 @@
background_player
popup_player
download
+ add_to_playlist
always_ask_player
@@ -367,6 +372,7 @@
- @string/background_player
- @string/popup_player
- @string/download
+ - @string/add_to_playlist
- @string/always_ask_open_action
@@ -375,11 +381,13 @@
- @string/background_player_key
- @string/popup_player_key
- @string/download_key
+ - @string/add_to_playlist_key
- @string/always_ask_open_action_key
update_app_key
+ manual_update_key
update_pref_screen_key
update_expiry_key
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 225ccd126f9..a8bb4c78883 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -52,6 +52,9 @@
org.xbmc.kore
Show \"Play with Kodi\" option
Display an option to play a video via Kodi media center
+ Crash the player
+ Report player errors
+ Reports player errors in full detail instead of showing a short-lived toast message (useful for diagnosing problems)
Scale thumbnail to 1:1 aspect ratio
Scale the video thumbnail shown in the notification from 16:9 to 1:1 aspect ratio (may introduce distortions)
First action button
@@ -439,7 +442,8 @@
New Playlist
Rename
Name
- Add to playlist
+ Add to playlist
+ Processing... May take a moment
Mute
Unmute
Set as playlist thumbnail
@@ -472,6 +476,8 @@
Show image indicators
Show Picasso colored ribbons on top of images indicating their source: red for network, blue for disk and green for memory
Crash the app
+ Show \"crash the player\"
+ Shows a crash option when using the player
Import
Import from
@@ -516,6 +522,8 @@
Updates
Show a notification to prompt app update when a new version is available
+ Check for updates
+ Manually check for new versions
Minimize on app switch
Action when switching to other app from main video player — %s
@@ -546,6 +554,7 @@
recovering
Queue
Action denied by the system
+ Checking for updates…
Download failed
@@ -632,6 +641,7 @@
Not loaded: %d
Loading feed…
Processing feed…
+ New feed items
Select subscriptions
No subscription selected
diff --git a/app/src/main/res/xml/debug_settings.xml b/app/src/main/res/xml/debug_settings.xml
index 22abebcae94..5e2cc28edc9 100644
--- a/app/src/main/res/xml/debug_settings.xml
+++ b/app/src/main/res/xml/debug_settings.xml
@@ -49,9 +49,26 @@
android:title="@string/show_image_indicators_title"
app:iconSpaceReserved="false" />
+
+
+
+
diff --git a/app/src/main/res/xml/update_settings.xml b/app/src/main/res/xml/update_settings.xml
index adaa4735276..ef121ec4efa 100644
--- a/app/src/main/res/xml/update_settings.xml
+++ b/app/src/main/res/xml/update_settings.xml
@@ -12,4 +12,11 @@
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
+
+
diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml
index f605fbe170e..4dc5d5c9aed 100644
--- a/app/src/main/res/xml/video_audio_settings.xml
+++ b/app/src/main/res/xml/video_audio_settings.xml
@@ -89,6 +89,7 @@
android:title="@string/show_play_with_kodi_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
+