|
83 | 83 | import org.schabi.newpipe.views.FocusOverlayView; |
84 | 84 |
|
85 | 85 | import java.io.Serializable; |
| 86 | +import java.lang.ref.WeakReference; |
86 | 87 | import java.util.ArrayList; |
87 | 88 | import java.util.Arrays; |
88 | 89 | import java.util.List; |
| 90 | +import java.util.Vector; |
89 | 91 |
|
90 | 92 | import icepick.Icepick; |
91 | 93 | import icepick.State; |
@@ -300,7 +302,7 @@ private static void handleError(final Context context, final ErrorInfo errorInfo |
300 | 302 | } |
301 | 303 | } |
302 | 304 |
|
303 | | - private void showUnsupportedUrlDialog(final String url) { |
| 305 | + protected void showUnsupportedUrlDialog(final String url) { |
304 | 306 | final Context context = getThemeWrapperContext(); |
305 | 307 | new AlertDialog.Builder(context) |
306 | 308 | .setTitle(R.string.unsupported_url) |
@@ -587,7 +589,7 @@ private List<AdapterChoiceItem> getChoicesForService(final StreamingService serv |
587 | 589 | return returnedItems; |
588 | 590 | } |
589 | 591 |
|
590 | | - private Context getThemeWrapperContext() { |
| 592 | + protected Context getThemeWrapperContext() { |
591 | 593 | return new ContextThemeWrapper(this, ThemeHelper.isLightThemeSelected(this) |
592 | 594 | ? R.style.LightTheme : R.style.DarkTheme); |
593 | 595 | } |
@@ -694,54 +696,175 @@ private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) { |
694 | 696 | return playerType == null || playerType == PlayerType.MAIN; |
695 | 697 | } |
696 | 698 |
|
| 699 | + public static class PersistentFragment extends Fragment { |
| 700 | + private WeakReference<AppCompatActivity> context; |
| 701 | + private boolean isPaused = true; |
| 702 | + private final Vector<ResultRunnable> buffer = new Vector<>(); |
| 703 | + private final CompositeDisposable disposables = new CompositeDisposable(); |
| 704 | + |
| 705 | + public interface ResultRunnable { |
| 706 | + void run(AppCompatActivity context); |
| 707 | + } |
| 708 | + |
| 709 | + @Override |
| 710 | + public void onAttach(@NonNull final Context activityContext) { |
| 711 | + super.onAttach(activityContext); |
| 712 | + context = new WeakReference<>((AppCompatActivity) activityContext); |
| 713 | + } |
| 714 | + |
| 715 | + @SuppressWarnings("deprecation") |
| 716 | + @Override |
| 717 | + public void onCreate(final Bundle savedInstanceState) { |
| 718 | + super.onCreate(savedInstanceState); |
| 719 | + setRetainInstance(true); |
| 720 | + } |
| 721 | + |
| 722 | + @Override |
| 723 | + public void onDestroy() { |
| 724 | + super.onDestroy(); |
| 725 | + disposables.clear(); |
| 726 | + } |
| 727 | + |
| 728 | + @Override |
| 729 | + public void onPause() { |
| 730 | + isPaused = true; |
| 731 | + super.onPause(); |
| 732 | + } |
| 733 | + |
| 734 | + @Override |
| 735 | + public void onResume() { |
| 736 | + isPaused = false; |
| 737 | + playback(); |
| 738 | + super.onResume(); |
| 739 | + } |
| 740 | + |
| 741 | + private AppCompatActivity getActivityContext() { |
| 742 | + return context == null ? null : context.get(); |
| 743 | + } |
| 744 | + |
| 745 | + // guard against IllegalStateException in calling DialogFragment.show() whilst in background |
| 746 | + // (which could happen, say, when the user pressed the home button while waiting for |
| 747 | + // the network request to return) when it internally calls FragmentTransaction.commit() |
| 748 | + // after the FragmentManager has saved its states (isStateSaved() == true) |
| 749 | + // (ref: https://stackoverflow.com/a/39813506) |
| 750 | + private void playback() { |
| 751 | + if (activityGone()) { |
| 752 | + done(); |
| 753 | + } |
| 754 | + if (buffer.size() == 0 || isPaused) { |
| 755 | + return; |
| 756 | + } |
| 757 | + while (buffer.size() > 0) { |
| 758 | + final ResultRunnable runnable = buffer.elementAt(0); |
| 759 | + buffer.removeElementAt(0); |
| 760 | + getActivityContext().runOnUiThread(() -> { |
| 761 | + // execute queued task with new context, in case activity has been recreated |
| 762 | + runnable.run(getActivityContext()); |
| 763 | + }); |
| 764 | + } |
| 765 | + done(); |
| 766 | + } |
| 767 | + private boolean activityGone() { |
| 768 | + return getActivityContext() == null || getActivityContext().isFinishing(); |
| 769 | + } |
| 770 | + |
| 771 | + // a DefaultLifecycleObserver is probably a good candidate here, but for now |
| 772 | + // let's stick with a vanilla approach to avoid pulling in an extra artifact just for this |
| 773 | + private void runOnVisible(final ResultRunnable runnable) { |
| 774 | + if (activityGone()) { |
| 775 | + done(); |
| 776 | + } |
| 777 | + if (isPaused) { |
| 778 | + buffer.add(runnable); |
| 779 | + if (!getActivityContext().isChangingConfigurations()) { |
| 780 | + // try to bring the activity back to front if minimised |
| 781 | + final Intent i = new Intent(getActivityContext(), RouterActivity.class); |
| 782 | + i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); |
| 783 | + startActivity(i); |
| 784 | + } |
| 785 | + } else { |
| 786 | + getActivityContext().runOnUiThread(() -> { |
| 787 | + runnable.run(getActivityContext()); |
| 788 | + done(); |
| 789 | + }); |
| 790 | + } |
| 791 | + } |
| 792 | + |
| 793 | + private void done() { |
| 794 | + if (getActivityContext() != null) { |
| 795 | + getActivityContext().getSupportFragmentManager() |
| 796 | + .beginTransaction().remove(this).commit(); |
| 797 | + } |
| 798 | + } |
| 799 | + |
| 800 | + @SuppressLint("CheckResult") |
| 801 | + protected void openDownloadDialog(final int currentServiceId, final String currentUrl) { |
| 802 | + disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) |
| 803 | + .subscribeOn(Schedulers.io()) |
| 804 | + .observeOn(AndroidSchedulers.mainThread()) |
| 805 | + .subscribe(result -> |
| 806 | + runOnVisible(ctx -> { |
| 807 | + final FragmentManager fm = ctx.getSupportFragmentManager(); |
| 808 | + final DownloadDialog downloadDialog = new DownloadDialog(ctx, result); |
| 809 | + // dismiss listener to be handled by FragmentManager |
| 810 | + downloadDialog.show(fm, "downloadDialog"); |
| 811 | + } |
| 812 | + ), throwable -> runOnVisible(ctx -> |
| 813 | + ((RouterActivity) ctx).showUnsupportedUrlDialog(currentUrl)))); |
| 814 | + } |
| 815 | + |
| 816 | + private void openAddToPlaylistDialog(final int currentServiceId, final String currentUrl) { |
| 817 | + disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false) |
| 818 | + .subscribeOn(Schedulers.io()) |
| 819 | + .observeOn(AndroidSchedulers.mainThread()) |
| 820 | + .subscribe( |
| 821 | + info -> runOnVisible(ctx -> |
| 822 | + PlaylistDialog.createCorrespondingDialog( |
| 823 | + ((RouterActivity) ctx).getThemeWrapperContext(), |
| 824 | + List.of(new StreamEntity(info)), |
| 825 | + playlistDialog -> { |
| 826 | + // dismiss listener to be handled by FragmentManager |
| 827 | + final FragmentManager fm = ctx.getSupportFragmentManager(); |
| 828 | + playlistDialog.show(fm, "addToPlaylistDialog"); |
| 829 | + } |
| 830 | + )), |
| 831 | + throwable -> runOnVisible(ctx -> handleError(ctx, new ErrorInfo( |
| 832 | + throwable, |
| 833 | + UserAction.REQUESTED_STREAM, |
| 834 | + "Tried to add " + currentUrl + " to a playlist", |
| 835 | + ((RouterActivity) ctx).currentService.getServiceId()) |
| 836 | + )) |
| 837 | + ) |
| 838 | + ); |
| 839 | + } |
| 840 | + } |
| 841 | + |
697 | 842 | private void openAddToPlaylistDialog() { |
698 | 843 | pleaseWait(); |
699 | 844 |
|
700 | | - disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, false) |
701 | | - .subscribeOn(Schedulers.io()) |
702 | | - .observeOn(AndroidSchedulers.mainThread()) |
703 | | - .subscribe( |
704 | | - info -> PlaylistDialog.createCorrespondingDialog( |
705 | | - getThemeWrapperContext(), |
706 | | - List.of(new StreamEntity(info)), |
707 | | - playlistDialog -> { |
708 | | - // to be handled by FragmentManager |
709 | | - // playlistDialog.setOnDismissListener(dialog ->finish()); |
710 | | - |
711 | | - final FragmentManager fm = getSupportFragmentManager(); |
712 | | - playlistDialog.show(fm, "addToPlaylistDialog"); |
713 | | - fm.executePendingTransactions(); |
714 | | - } |
715 | | - ), |
716 | | - throwable -> handleError(this, new ErrorInfo( |
717 | | - throwable, |
718 | | - UserAction.REQUESTED_STREAM, |
719 | | - "Tried to add " + currentUrl + " to a playlist", |
720 | | - currentService.getServiceId()) |
721 | | - ) |
722 | | - ) |
723 | | - ); |
| 845 | + getPersistFragment().openAddToPlaylistDialog(currentServiceId, currentUrl); |
724 | 846 | } |
725 | 847 |
|
726 | | - @SuppressLint("CheckResult") |
727 | 848 | private void openDownloadDialog() { |
728 | 849 | pleaseWait(); |
729 | 850 |
|
730 | | - disposables.add(ExtractorHelper.getStreamInfo(currentServiceId, currentUrl, true) |
731 | | - .subscribeOn(Schedulers.io()) |
732 | | - .observeOn(AndroidSchedulers.mainThread()) |
733 | | - .subscribe(result -> { |
734 | | - final DownloadDialog downloadDialog = new DownloadDialog(this, result); |
735 | | - // to be handled by FragmentManager since listener would be gone when recreated |
736 | | - // playlistDialog.setOnDismissListener(dialog ->finish()); |
737 | | - |
738 | | - final FragmentManager fm = getSupportFragmentManager(); |
739 | | - downloadDialog.show(fm, "downloadDialog"); |
740 | | - fm.executePendingTransactions(); |
741 | | - }, throwable -> showUnsupportedUrlDialog(currentUrl))); |
| 851 | + getPersistFragment().openDownloadDialog(currentServiceId, currentUrl); |
| 852 | + } |
| 853 | + |
| 854 | + private PersistentFragment getPersistFragment() { |
| 855 | + final FragmentManager fm = getSupportFragmentManager(); |
| 856 | + PersistentFragment persistFragment = |
| 857 | + (PersistentFragment) fm.findFragmentByTag("PERSIST_FRAGMENT"); |
| 858 | + if (persistFragment == null) { |
| 859 | + persistFragment = new PersistentFragment(); |
| 860 | + fm.beginTransaction() |
| 861 | + .add(persistFragment, "PERSIST_FRAGMENT") |
| 862 | + .commitNow(); |
| 863 | + } |
| 864 | + return persistFragment; |
742 | 865 | } |
743 | 866 |
|
744 | | - private void pleaseWait() { |
| 867 | + protected void pleaseWait() { |
745 | 868 | // Getting the stream info usually takes a moment |
746 | 869 | // Notifying the user here to ensure that no confusion arises |
747 | 870 | Toast.makeText( |
|
0 commit comments