diff --git a/.gitignore b/.gitignore index 5c6962be1..40e7d2c03 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 53edac5e4..000000000 --- a/app/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.gitignore -/build -*.iml diff --git a/app/build.gradle b/app/build.gradle index 468006578..35bdd998e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -17,8 +17,8 @@ android { resValue "string", "app_name", "NewPipe" minSdkVersion 19 targetSdkVersion 29 - versionCode 979 - versionName "0.21.13" + versionCode 980 + versionName "0.21.14" multiDexEnabled true @@ -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:v0.21.12' /** 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 d92d0b5bf..cc631af7a 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 433c155c2..639443377 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 @@ import java.util.ArrayList; *
  • {@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 abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt mFragments.set(position, null); mCurTransaction.remove(fragment); - if (fragment == mCurrentPrimaryItem) { + if (fragment.equals(mCurrentPrimaryItem)) { mCurrentPrimaryItem = null; } } @@ -247,7 +252,19 @@ public abstract class FragmentStatePagerAdapterMenuWorkaround extends PagerAdapt @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 0be427648..16ddb8376 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 abstract class BaseFragment extends Fragment { AppWatcher.INSTANCE.getObjectWatcher().watch(this); } - @Override - public void setUserVisibleHint(final boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - mIsVisibleToUser = isVisibleToUser; - } - /*////////////////////////////////////////////////////////////////////////// // Init //////////////////////////////////////////////////////////////////////////*/ @@ -109,8 +102,7 @@ public abstract class BaseFragment extends Fragment { 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 9c392be1e..9e43394ac 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.Intent; 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.NonNull; 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 final class CheckForNewAppVersion extends IntentService { 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 @@ public final class CheckForNewAppVersion extends IntentService { 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); - - final String channelId = application - .getString(R.string.app_update_notification_channel_id); - final NotificationCompat.Builder notificationBuilder - = new NotificationCompat.Builder(application, channelId) - .setSmallIcon(R.drawable.ic_newpipe_update) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setContentIntent(pendingIntent) - .setAutoCancel(true) - .setContentTitle(application - .getString(R.string.app_update_notification_content_title)) - .setContentText(application - .getString(R.string.app_update_notification_content_text) - + " " + versionName); - - final NotificationManagerCompat notificationManager - = NotificationManagerCompat.from(application); - notificationManager.notify(notificationId, notificationBuilder.build()); + if (BuildConfig.VERSION_CODE >= versionCode) { + return; } + + // A pending intent to open the apk location url in the browser. + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(apkLocationUrl)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + final PendingIntent pendingIntent + = PendingIntent.getActivity(application, 0, intent, 0); + + final String channelId = application + .getString(R.string.app_update_notification_channel_id); + final NotificationCompat.Builder notificationBuilder + = new NotificationCompat.Builder(application, channelId) + .setSmallIcon(R.drawable.ic_newpipe_update) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setContentIntent(pendingIntent) + .setAutoCancel(true) + .setContentTitle(application + .getString(R.string.app_update_notification_content_title)) + .setContentText(application + .getString(R.string.app_update_notification_content_text) + + " " + versionName); + + final NotificationManagerCompat notificationManager + = NotificationManagerCompat.from(application); + notificationManager.notify(2000, notificationBuilder.build()); } - 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(); - } - - 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 @@ public final class CheckForNewAppVersion extends IntentService { 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 @@ public final class CheckForNewAppVersion extends IntentService { // 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 2a8872e26..0a49e00e4 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 @@ public class MainActivity extends AppCompatActivity { @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 @@ public class MainActivity extends AppCompatActivity { 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 @@ public class MainActivity extends AppCompatActivity { 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 @@ public class MainActivity extends AppCompatActivity { } 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 class MainActivity extends AppCompatActivity { 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 9105ff992..fcad0b612 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 android.widget.PopupMenu; 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 org.schabi.newpipe.util.NavigationHelper; 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 final class QueueItemMenuUtil { 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 final class QueueItemMenuUtil { 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 feb9e029d..4e96f3bb6 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.core.widget.TextViewCompat; 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.playlist.PlaylistInfo; 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.ExtractorHelper; 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.disposables.Disposable; 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 @@ public class RouterActivity extends AppCompatActivity { .setNegativeButton(R.string.just_once, dialogButtonsClickListener) .setPositiveButton(R.string.always, dialogButtonsClickListener) .setOnDismissListener((dialog) -> { - if (!selectionIsDownload) { + if (!selectionIsDownload && !selectionIsAddToPlaylist) { finish(); } }) @@ -446,6 +450,10 @@ public class RouterActivity extends AppCompatActivity { 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 @@ public class RouterActivity extends AppCompatActivity { 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 @@ public class RouterActivity extends AppCompatActivity { 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 @@ public class RouterActivity extends AppCompatActivity { 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 689f1ead6..72692a9f5 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 000000000..db94de5e5 --- /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 c0d88c8ec..db3a92d4f 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 0cccfa4fe..fe4eef37a 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 class BlankFragment extends BaseFragment { } @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 437cb2fdb..8c6e01537 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 com.squareup.picasso.Callback; 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.EmptyFragment; 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.KoreUtils; 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 final class VideoDetailFragment 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 @@ public final class VideoDetailFragment // 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 @@ public final class VideoDetailFragment 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 @@ public final class VideoDetailFragment 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 000000000..9309a8a49 --- /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 c30b6fc05..b9065c969 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 @@ public abstract class BaseListFragment extends BaseStateFragment 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 @@ public abstract class BaseListFragment extends BaseStateFragment 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 1d16559ac..30e38a966 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 class ChannelFragment extends BaseListInfoFragment } @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 f37f487bf..c25f18e8b 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 class KioskFragment extends BaseListInfoFragment { } @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 class KioskFragment extends BaseListInfoFragment { 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 f3aa2e306..a8763af73 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 @@ public class PlaylistFragment extends BaseListInfoFragment { 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 7de212383..d4d73f74f 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 class SearchFragment extends BaseListFragment { - 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 final class PlaylistAppendDialog extends PlaylistDialog { 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 final class PlaylistAppendDialog extends PlaylistDialog { //////////////////////////////////////////////////////////////////////////*/ 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 @@ public final class PlaylistAppendDialog extends PlaylistDialog { 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 f48c72d04..6664144cd 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 android.widget.Toast; 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 final class PlaylistCreationDialog extends PlaylistDialog { @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 final class PlaylistCreationDialog extends PlaylistDialog { 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 2c2987e95..c2d4474f8 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.NonNull; 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 abstract class PlaylistDialog extends DialogFragment implements StateSave StateSaver.onDestroy(savedState); } + public List getStreamEntities() { + return streamEntities; + } + @NonNull @Override public Dialog onCreateDialog(final Bundle savedInstanceState) { @@ -55,6 +67,14 @@ public abstract class PlaylistDialog extends DialogFragment implements StateSave 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 abstract class PlaylistDialog extends DialogFragment implements StateSave 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 ff7c2848e..e28f2d31a 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 965075bf3..5b593bcd7 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 8bdf412b5..2cbf9ad05 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 0d2caf126..217e3f3e3 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 823e56d9e..d94088cd0 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 class HistoryRecordManager { } // 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 class HistoryRecordManager { .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 class HistoryRecordManager { .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 9632b47f7..43a5fcf3c 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 class StatisticsPlaylistFragment } @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 @@ public class StatisticsPlaylistFragment 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 21da9e571..87d913b3b 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 class LocalPlaylistFragment extends BaseLocalListFragment 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 22e66e793..2d8c1a830 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.ProgressBar; 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.C; 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.BehindLiveWindowException; 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.MediaSourceManager; 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.MediaSourceTag; 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.Disposable; 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 final class Player implements 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 @@ public final class Player implements // 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 @@ public final class Player implements + "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 @@ public final class Player implements }, 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 @@ public final class Player implements 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 @@ public final class Player implements if (!exoPlayerIsNull()) { simpleExoPlayer.removeListener(this); - simpleExoPlayer.removeVideoListener(this); simpleExoPlayer.stop(); simpleExoPlayer.release(); } @@ -858,10 +859,10 @@ public final class Player implements 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 final class Player implements 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 final class Player implements } @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 final class Player implements // 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 final class Player implements 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 final class Player implements //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 final class Player implements * * * @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 @@ public final class Player implements 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 @@ public final class Player implements } @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 @@ public final class Player implements } 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 998324c9c..e55c596b8 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 class PlayerGestureListener 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(); - } + if (player.isControlsVisible()) { + player.hideControls(150, 0); + return; + } + // -- Controls are not visible -- - } else /* playerType == MainPlayer.PlayerType.VIDEO */ { - - 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 class PlayerGestureListener + 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 class PlayerGestureListener } } 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 class PlayerGestureListener 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 class PlayerGestureListener 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 2e2fda86c..b36f9f234 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 androidx.media.AudioManagerCompat; 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 class AudioReactor implements AudioManager.OnAudioFocusChangeListener, An //////////////////////////////////////////////////////////////////////////*/ @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 71cfcc818..ec0e4e4a7 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 class LoadController implements LoadControl { 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 ef0d84029..8d344c877 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 @@ public class MediaSessionManager { // 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 @@ public class MediaSessionManager { } + @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 bbe281921..5139ef9cd 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 androidx.preference.PreferenceManager; 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 class PlaybackParameterDialog extends DialogFragment { 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 5fea4761b..b7584151d 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 @@ import com.google.android.exoplayer2.upstream.TransferListener; 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 class PlayerDataSource { } 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) + 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( + getDefaultDashChunkSourceFactory(cachelessDataSourceFactory), + cachelessDataSourceFactory + ) .setLoadErrorHandlingPolicy( new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)); } - public DashMediaSource.Factory getLiveDashMediaSourceFactory() { - return new DashMediaSource.Factory(new DefaultDashChunkSource.Factory( - cachelessDataSourceFactory), cachelessDataSourceFactory) - .setLoadErrorHandlingPolicy( - new DefaultLoadErrorHandlingPolicy(MANIFEST_MINIMUM_RETRY)) - .setLivePresentationDelayMs(LIVE_STREAM_EDGE_GAP_MILLIS, true); - } + 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 + ); + } - public SsMediaSource.Factory getSsMediaSourceFactory() { - return new SsMediaSource.Factory(new DefaultSsChunkSource.Factory( - cacheDataSourceFactory), cacheDataSourceFactory); + 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 828833a8d..c51b6d5dd 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 androidx.preference.PreferenceManager; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player.RepeatMode; import com.google.android.exoplayer2.SeekParameters; -import com.google.android.exoplayer2.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.Objects; 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 final class PlayerHelper { 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 d70707fdb..389be7062 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.RendererCapabilities.Capabilities; 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 class CustomTrackSelector extends DefaultTrackSelector { @Override @Nullable - protected Pair selectTextTrack( + protected Pair selectTextTrack( final TrackGroupArray groups, @NonNull final int[][] formatSupport, @NonNull final Parameters params, @@ -86,7 +86,7 @@ public class CustomTrackSelector extends DefaultTrackSelector { } } 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 000000000..626200ae1 --- /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 014c13339..f2259b120 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 abstract class PlayQueue implements Serializable { * 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 5fee43659..b283e105e 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 abstract class PlayQueueItemTouchCallback extends ItemTouchHelper.SimpleC @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 81e629c2f..cfe9dbb62 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 androidx.annotation.Nullable; 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 @@ public interface PlaybackResolver extends Resolver { @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 @@ public interface PlaybackResolver extends Resolver { @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 6bc63a56a..3ac275695 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 2f65af4d6..12599b828 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 5f388efb7..dfc053a62 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 class PeertubeInstanceListFragment extends Fragment { 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 class PeertubeInstanceListFragment extends Fragment { @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 d2f56b487..bc183d08a 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 6e50765ba..c9eb42fca 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 class ChooseTabsFragment extends Fragment { 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 class ChooseTabsFragment extends Fragment { @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 bdf5e8ce4..bbe9a7edb 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.pm.PackageManager; 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 final class DeviceUtils { 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 cb796a7a6..c01e051b0 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 final class NavigationHelper { 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 final class NavigationHelper { 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 final class NavigationHelper { 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 000000000..cf1a9a03a --- /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 ec51cc370..0ffbe4137 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.net.Uri; 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 enum StreamDialogEntry { 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 8cf5a4cdf..05e69408a 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.HttpsURLConnection; 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 @@ import static org.schabi.newpipe.MainActivity.DEBUG; */ public class TLSSocketFactoryCompat extends SSLSocketFactory { + private static final String TAG = "TLSSocketFactoryCom"; private static TLSSocketFactoryCompat instance = null; @@ -32,14 +32,6 @@ public class TLSSocketFactoryCompat extends SSLSocketFactory { 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 class TLSSocketFactoryCompat extends SSLSocketFactory { 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 000000000..2adc28d0e --- /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 000000000..8fdac32db --- /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 4b79d92f6..c2359552e 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 b64a9241c..99c46e552 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 873f3c884..9e2257539 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"> - - - - - - - - - - - +