1010import android .content .Intent ;
1111import android .content .SharedPreferences ;
1212import android .content .pm .PackageManager ;
13+ import android .os .Build ;
1314import android .os .Bundle ;
1415import android .text .TextUtils ;
1516import android .view .ContextThemeWrapper ;
1617import android .view .LayoutInflater ;
1718import android .view .View ;
1819import android .view .ViewGroup ;
20+ import android .view .WindowManager ;
1921import android .widget .Button ;
2022import android .widget .RadioButton ;
2123import android .widget .RadioGroup ;
3133import androidx .core .app .NotificationCompat ;
3234import androidx .core .app .ServiceCompat ;
3335import androidx .core .math .MathUtils ;
36+ import androidx .fragment .app .DialogFragment ;
37+ import androidx .fragment .app .Fragment ;
3438import androidx .fragment .app .FragmentManager ;
39+ import androidx .lifecycle .DefaultLifecycleObserver ;
40+ import androidx .lifecycle .Lifecycle ;
41+ import androidx .lifecycle .LifecycleOwner ;
3542import androidx .preference .PreferenceManager ;
3643
3744import org .schabi .newpipe .database .stream .model .StreamEntity ;
8087import org .schabi .newpipe .views .FocusOverlayView ;
8188
8289import java .io .Serializable ;
90+ import java .lang .ref .WeakReference ;
8391import java .util .ArrayList ;
8492import java .util .Arrays ;
8593import java .util .List ;
94+ import java .util .Optional ;
95+ import java .util .function .Consumer ;
8696
8797import icepick .Icepick ;
8898import icepick .State ;
91101import io .reactivex .rxjava3 .core .Single ;
92102import io .reactivex .rxjava3 .disposables .CompositeDisposable ;
93103import io .reactivex .rxjava3 .disposables .Disposable ;
94- import io .reactivex .rxjava3 .functions .Consumer ;
95104import io .reactivex .rxjava3 .schedulers .Schedulers ;
96105
97106/**
@@ -111,12 +120,57 @@ public class RouterActivity extends AppCompatActivity {
111120 private boolean selectionIsDownload = false ;
112121 private boolean selectionIsAddToPlaylist = false ;
113122 private AlertDialog alertDialogChoice = null ;
123+ private FragmentManager .FragmentLifecycleCallbacks dismissListener = null ;
114124
115125 @ Override
116126 protected void onCreate (final Bundle savedInstanceState ) {
127+ ThemeHelper .setDayNightMode (this );
128+ setTheme (ThemeHelper .isLightThemeSelected (this )
129+ ? R .style .RouterActivityThemeLight : R .style .RouterActivityThemeDark );
130+ Localization .assureCorrectAppLanguage (this );
131+
132+ // Pass-through touch events to background activities
133+ // so that our transparent window won't lock UI in the mean time
134+ // network request is underway before showing PlaylistDialog or DownloadDialog
135+ // (ref: https://stackoverflow.com/a/10606141)
136+ getWindow ().addFlags (WindowManager .LayoutParams .FLAG_NOT_FOCUSABLE
137+ | WindowManager .LayoutParams .FLAG_NOT_TOUCH_MODAL
138+ | WindowManager .LayoutParams .FLAG_NOT_TOUCHABLE );
139+
140+ // Android never fails to impress us with a list of new restrictions per API.
141+ // Starting with S (Android 12) one of the prerequisite conditions has to be met
142+ // before the FLAG_NOT_TOUCHABLE flag is allowed to kick in:
143+ // @see WindowManager.LayoutParams#FLAG_NOT_TOUCHABLE
144+ // For our present purpose it seems we can just set LayoutParams.alpha to 0
145+ // on the strength of "4. Fully transparent windows" without affecting the scrim of dialogs
146+ final WindowManager .LayoutParams params = getWindow ().getAttributes ();
147+ params .alpha = 0f ;
148+ getWindow ().setAttributes (params );
149+
117150 super .onCreate (savedInstanceState );
118151 Icepick .restoreInstanceState (this , savedInstanceState );
119152
153+ // FragmentManager will take care to recreate (Playlist|Download)Dialog when screen rotates
154+ // We used to .setOnDismissListener(dialog -> finish()); when creating these DialogFragments
155+ // but those callbacks won't survive a config change
156+ // Try an alternate approach to hook into FragmentManager instead, to that effect
157+ // (ref: https://stackoverflow.com/a/44028453)
158+ final FragmentManager fm = getSupportFragmentManager ();
159+ if (dismissListener == null ) {
160+ dismissListener = new FragmentManager .FragmentLifecycleCallbacks () {
161+ @ Override
162+ public void onFragmentDestroyed (@ NonNull final FragmentManager fm ,
163+ @ NonNull final Fragment f ) {
164+ super .onFragmentDestroyed (fm , f );
165+ if (f instanceof DialogFragment && fm .getFragments ().isEmpty ()) {
166+ // No more DialogFragments, we're done
167+ finish ();
168+ }
169+ }
170+ };
171+ }
172+ fm .registerFragmentLifecycleCallbacks (dismissListener , false );
173+
120174 if (TextUtils .isEmpty (currentUrl )) {
121175 currentUrl = getUrl (getIntent ());
122176
@@ -125,11 +179,6 @@ protected void onCreate(final Bundle savedInstanceState) {
125179 finish ();
126180 }
127181 }
128-
129- ThemeHelper .setDayNightMode (this );
130- setTheme (ThemeHelper .isLightThemeSelected (this )
131- ? R .style .RouterActivityThemeLight : R .style .RouterActivityThemeDark );
132- Localization .assureCorrectAppLanguage (this );
133182 }
134183
135184 @ Override
@@ -151,16 +200,34 @@ protected void onSaveInstanceState(@NonNull final Bundle outState) {
151200 protected void onStart () {
152201 super .onStart ();
153202
154- handleUrl (currentUrl );
203+ // Don't overlap the DialogFragment after rotating the screen
204+ // If there's no DialogFragment, we're either starting afresh
205+ // or we didn't make it to PlaylistDialog or DownloadDialog before the orientation change
206+ if (getSupportFragmentManager ().getFragments ().isEmpty ()) {
207+ // Start over from scratch
208+ handleUrl (currentUrl );
209+ }
155210 }
156211
157212 @ Override
158213 protected void onDestroy () {
159214 super .onDestroy ();
160215
216+ if (dismissListener != null ) {
217+ getSupportFragmentManager ().unregisterFragmentLifecycleCallbacks (dismissListener );
218+ }
219+
161220 disposables .clear ();
162221 }
163222
223+ @ Override
224+ public void finish () {
225+ // allow the activity to recreate in case orientation changes
226+ if (!isChangingConfigurations ()) {
227+ super .finish ();
228+ }
229+ }
230+
164231 private void handleUrl (final String url ) {
165232 disposables .add (Observable
166233 .fromCallable (() -> {
@@ -240,7 +307,7 @@ private static void handleError(final Context context, final ErrorInfo errorInfo
240307 }
241308 }
242309
243- private void showUnsupportedUrlDialog (final String url ) {
310+ protected void showUnsupportedUrlDialog (final String url ) {
244311 final Context context = getThemeWrapperContext ();
245312 new AlertDialog .Builder (context )
246313 .setTitle (R .string .unsupported_url )
@@ -527,7 +594,7 @@ private List<AdapterChoiceItem> getChoicesForService(final StreamingService serv
527594 return returnedItems ;
528595 }
529596
530- private Context getThemeWrapperContext () {
597+ protected Context getThemeWrapperContext () {
531598 return new ContextThemeWrapper (this , ThemeHelper .isLightThemeSelected (this )
532599 ? R .style .LightTheme : R .style .DarkTheme );
533600 }
@@ -634,54 +701,179 @@ private boolean canHandleChoiceLikeShowInfo(final String selectedChoiceKey) {
634701 return playerType == null || playerType == PlayerType .MAIN ;
635702 }
636703
637- private void openAddToPlaylistDialog () {
638- // Getting the stream info usually takes a moment
639- // Notifying the user here to ensure that no confusion arises
640- Toast .makeText (
641- getApplicationContext (),
642- getString (R .string .processing_may_take_a_moment ),
643- Toast .LENGTH_SHORT )
644- .show ();
704+ public static class PersistentFragment extends Fragment {
705+ private WeakReference <AppCompatActivity > weakContext ;
706+ private final CompositeDisposable disposables = new CompositeDisposable ();
707+ private int running = 0 ;
645708
646- disposables .add (ExtractorHelper .getStreamInfo (currentServiceId , currentUrl , false )
647- .subscribeOn (Schedulers .io ())
648- .observeOn (AndroidSchedulers .mainThread ())
649- .subscribe (
650- info -> PlaylistDialog .createCorrespondingDialog (
651- getThemeWrapperContext (),
652- List .of (new StreamEntity (info )),
653- playlistDialog -> {
654- playlistDialog .setOnDismissListener (dialog -> finish ());
655-
656- playlistDialog .show (
657- this .getSupportFragmentManager (),
658- "addToPlaylistDialog"
659- );
660- }
661- ),
662- throwable -> handleError (this , new ErrorInfo (
663- throwable ,
664- UserAction .REQUESTED_STREAM ,
665- "Tried to add " + currentUrl + " to a playlist" ,
666- currentService .getServiceId ())
667- )
668- )
669- );
709+ private synchronized void inFlight (final boolean started ) {
710+ if (started ) {
711+ running ++;
712+ } else {
713+ running --;
714+ if (running <= 0 ) {
715+ getActivityContext ().ifPresent (context -> context .getSupportFragmentManager ()
716+ .beginTransaction ().remove (this ).commit ());
717+ }
718+ }
719+ }
720+
721+ @ Override
722+ public void onAttach (@ NonNull final Context activityContext ) {
723+ super .onAttach (activityContext );
724+ weakContext = new WeakReference <>((AppCompatActivity ) activityContext );
725+ }
726+
727+ @ Override
728+ public void onDetach () {
729+ super .onDetach ();
730+ weakContext = null ;
731+ }
732+
733+ @ SuppressWarnings ("deprecation" )
734+ @ Override
735+ public void onCreate (final Bundle savedInstanceState ) {
736+ super .onCreate (savedInstanceState );
737+ setRetainInstance (true );
738+ }
739+
740+ @ Override
741+ public void onDestroy () {
742+ super .onDestroy ();
743+ disposables .clear ();
744+ }
745+
746+ /**
747+ * @return the activity context, if there is one and the activity is not finishing
748+ */
749+ private Optional <AppCompatActivity > getActivityContext () {
750+ return Optional .ofNullable (weakContext )
751+ .flatMap (context -> Optional .ofNullable (context .get ()))
752+ .filter (context -> !context .isFinishing ());
753+ }
754+
755+ // guard against IllegalStateException in calling DialogFragment.show() whilst in background
756+ // (which could happen, say, when the user pressed the home button while waiting for
757+ // the network request to return) when it internally calls FragmentTransaction.commit()
758+ // after the FragmentManager has saved its states (isStateSaved() == true)
759+ // (ref: https://stackoverflow.com/a/39813506)
760+ private void runOnVisible (final Consumer <AppCompatActivity > runnable ) {
761+ getActivityContext ().ifPresentOrElse (context -> {
762+ if (getLifecycle ().getCurrentState ().isAtLeast (Lifecycle .State .STARTED )) {
763+ context .runOnUiThread (() -> {
764+ runnable .accept (context );
765+ inFlight (false );
766+ });
767+ } else {
768+ getLifecycle ().addObserver (new DefaultLifecycleObserver () {
769+ @ Override
770+ public void onResume (@ NonNull final LifecycleOwner owner ) {
771+ getLifecycle ().removeObserver (this );
772+ getActivityContext ().ifPresentOrElse (context ->
773+ context .runOnUiThread (() -> {
774+ runnable .accept (context );
775+ inFlight (false );
776+ }),
777+ () -> inFlight (false )
778+ );
779+ }
780+ });
781+ // this trick doesn't seem to work on Android 10+ (API 29)
782+ // which places restrictions on starting activities from the background
783+ if (Build .VERSION .SDK_INT < Build .VERSION_CODES .Q
784+ && !context .isChangingConfigurations ()) {
785+ // try to bring the activity back to front if minimised
786+ final Intent i = new Intent (context , RouterActivity .class );
787+ i .setFlags (Intent .FLAG_ACTIVITY_REORDER_TO_FRONT );
788+ startActivity (i );
789+ }
790+ }
791+
792+ }, () -> {
793+ // this branch is executed if there is no activity context
794+ inFlight (false );
795+ });
796+ }
797+
798+ <T > Single <T > pleaseWait (final Single <T > single ) {
799+ // 'abuse' ambWith() here to cancel the toast for us when the wait is over
800+ return single .ambWith (Single .create (emitter -> getActivityContext ().ifPresent (context ->
801+ context .runOnUiThread (() -> {
802+ // Getting the stream info usually takes a moment
803+ // Notifying the user here to ensure that no confusion arises
804+ final Toast toast = Toast .makeText (context ,
805+ getString (R .string .processing_may_take_a_moment ),
806+ Toast .LENGTH_LONG );
807+ toast .show ();
808+ emitter .setCancellable (toast ::cancel );
809+ }))));
810+ }
811+
812+ @ SuppressLint ("CheckResult" )
813+ private void openDownloadDialog (final int currentServiceId , final String currentUrl ) {
814+ inFlight (true );
815+ disposables .add (ExtractorHelper .getStreamInfo (currentServiceId , currentUrl , true )
816+ .subscribeOn (Schedulers .io ())
817+ .observeOn (AndroidSchedulers .mainThread ())
818+ .compose (this ::pleaseWait )
819+ .subscribe (result ->
820+ runOnVisible (ctx -> {
821+ final FragmentManager fm = ctx .getSupportFragmentManager ();
822+ final DownloadDialog downloadDialog = new DownloadDialog (ctx , result );
823+ // dismiss listener to be handled by FragmentManager
824+ downloadDialog .show (fm , "downloadDialog" );
825+ }
826+ ), throwable -> runOnVisible (ctx ->
827+ ((RouterActivity ) ctx ).showUnsupportedUrlDialog (currentUrl ))));
828+ }
829+
830+ private void openAddToPlaylistDialog (final int currentServiceId , final String currentUrl ) {
831+ inFlight (true );
832+ disposables .add (ExtractorHelper .getStreamInfo (currentServiceId , currentUrl , false )
833+ .subscribeOn (Schedulers .io ())
834+ .observeOn (AndroidSchedulers .mainThread ())
835+ .compose (this ::pleaseWait )
836+ .subscribe (
837+ info -> getActivityContext ().ifPresent (context ->
838+ PlaylistDialog .createCorrespondingDialog (context ,
839+ List .of (new StreamEntity (info )),
840+ playlistDialog -> runOnVisible (ctx -> {
841+ // dismiss listener to be handled by FragmentManager
842+ final FragmentManager fm =
843+ ctx .getSupportFragmentManager ();
844+ playlistDialog .show (fm , "addToPlaylistDialog" );
845+ })
846+ )),
847+ throwable -> runOnVisible (ctx -> handleError (ctx , new ErrorInfo (
848+ throwable ,
849+ UserAction .REQUESTED_STREAM ,
850+ "Tried to add " + currentUrl + " to a playlist" ,
851+ ((RouterActivity ) ctx ).currentService .getServiceId ())
852+ ))
853+ )
854+ );
855+ }
856+ }
857+
858+ private void openAddToPlaylistDialog () {
859+ getPersistFragment ().openAddToPlaylistDialog (currentServiceId , currentUrl );
670860 }
671861
672- @ SuppressLint ("CheckResult" )
673862 private void openDownloadDialog () {
674- disposables .add (ExtractorHelper .getStreamInfo (currentServiceId , currentUrl , true )
675- .subscribeOn (Schedulers .io ())
676- .observeOn (AndroidSchedulers .mainThread ())
677- .subscribe (result -> {
678- final DownloadDialog downloadDialog = new DownloadDialog (this , result );
679- downloadDialog .setOnDismissListener (dialog -> finish ());
680-
681- final FragmentManager fm = getSupportFragmentManager ();
682- downloadDialog .show (fm , "downloadDialog" );
683- fm .executePendingTransactions ();
684- }, throwable -> showUnsupportedUrlDialog (currentUrl )));
863+ getPersistFragment ().openDownloadDialog (currentServiceId , currentUrl );
864+ }
865+
866+ private PersistentFragment getPersistFragment () {
867+ final FragmentManager fm = getSupportFragmentManager ();
868+ PersistentFragment persistFragment =
869+ (PersistentFragment ) fm .findFragmentByTag ("PERSIST_FRAGMENT" );
870+ if (persistFragment == null ) {
871+ persistFragment = new PersistentFragment ();
872+ fm .beginTransaction ()
873+ .add (persistFragment , "PERSIST_FRAGMENT" )
874+ .commitNow ();
875+ }
876+ return persistFragment ;
685877 }
686878
687879 @ Override
0 commit comments