Skip to content

Commit eed44b3

Browse files
authored
Merge pull request TeamNewPipe#9135 from devlearner/routeractivity-screen-rotate
Improve screen rotation handling in Open action menu
2 parents 8797669 + 944e295 commit eed44b3

1 file changed

Lines changed: 245 additions & 53 deletions

File tree

app/src/main/java/org/schabi/newpipe/RouterActivity.java

Lines changed: 245 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
import android.content.Intent;
1111
import android.content.SharedPreferences;
1212
import android.content.pm.PackageManager;
13+
import android.os.Build;
1314
import android.os.Bundle;
1415
import android.text.TextUtils;
1516
import android.view.ContextThemeWrapper;
1617
import android.view.LayoutInflater;
1718
import android.view.View;
1819
import android.view.ViewGroup;
20+
import android.view.WindowManager;
1921
import android.widget.Button;
2022
import android.widget.RadioButton;
2123
import android.widget.RadioGroup;
@@ -31,7 +33,12 @@
3133
import androidx.core.app.NotificationCompat;
3234
import androidx.core.app.ServiceCompat;
3335
import androidx.core.math.MathUtils;
36+
import androidx.fragment.app.DialogFragment;
37+
import androidx.fragment.app.Fragment;
3438
import androidx.fragment.app.FragmentManager;
39+
import androidx.lifecycle.DefaultLifecycleObserver;
40+
import androidx.lifecycle.Lifecycle;
41+
import androidx.lifecycle.LifecycleOwner;
3542
import androidx.preference.PreferenceManager;
3643

3744
import org.schabi.newpipe.database.stream.model.StreamEntity;
@@ -80,9 +87,12 @@
8087
import org.schabi.newpipe.views.FocusOverlayView;
8188

8289
import java.io.Serializable;
90+
import java.lang.ref.WeakReference;
8391
import java.util.ArrayList;
8492
import java.util.Arrays;
8593
import java.util.List;
94+
import java.util.Optional;
95+
import java.util.function.Consumer;
8696

8797
import icepick.Icepick;
8898
import icepick.State;
@@ -91,7 +101,6 @@
91101
import io.reactivex.rxjava3.core.Single;
92102
import io.reactivex.rxjava3.disposables.CompositeDisposable;
93103
import io.reactivex.rxjava3.disposables.Disposable;
94-
import io.reactivex.rxjava3.functions.Consumer;
95104
import 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

Comments
 (0)