fix(player): exit on phone back even with the chrome up (#1938)

4443b761 staged the system-back path (strip/fullscreen/hide/exit), so on
Android a back with the controls visible hid them instead of closing the
player. The PlayerNavigationCoordinator now takes an exitPlayerBeforeChrome
policy; the player enables it on mobile, restoring immediate exit on Back
while TV/desktop keep the staged chrome handling.
This commit is contained in:
edde746
2026-08-15 14:02:07 +02:00
parent 8383c7b73b
commit d623c2164f
3 changed files with 54 additions and 0 deletions
+5
View File
@@ -927,6 +927,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
dismissPrompt: _dismissPlaybackPromptForBack,
isChromePresented: () =>
_isPlayerInitialized && player != null && _hasFirstFrame.value && _chromeController.controlsPresented,
// On phones a Back must exit the player even with the controls up
// (#1938); the staged chrome handling is TV/desktop behavior (#4443b761
// applied it to the phone system-back path too, so back stopped
// closing the player).
exitPlayerBeforeChrome: () => PlatformDetector.isMobile(context),
exitFullscreenIfActive: FullscreenStateManager().exitFullscreenIfActive,
// macOS fullscreen belongs to the app window, while HTPC-style player
// navigation treats physical Escape as semantic Back. In both cases the
@@ -300,6 +300,7 @@ class PlayerNavigationCoordinator {
final bool Function() isChromePresented;
final Future<bool> Function() exitFullscreenIfActive;
final bool Function() _physicalEscapeExitsFullscreen;
final bool Function() _exitPlayerBeforeChrome;
final VoidCallback exitPlayer;
final VoidCallback navigateHome;
final bool Function() isActive;
@@ -313,14 +314,17 @@ class PlayerNavigationCoordinator {
required this.isChromePresented,
required this.exitFullscreenIfActive,
bool Function()? physicalEscapeExitsFullscreen,
bool Function()? exitPlayerBeforeChrome,
required this.exitPlayer,
required this.navigateHome,
bool Function()? isActive,
}) : _physicalEscapeExitsFullscreen = physicalEscapeExitsFullscreen ?? _alwaysTrue,
_exitPlayerBeforeChrome = exitPlayerBeforeChrome ?? _alwaysFalse,
isActive = isActive ?? _alwaysActive;
static bool _alwaysActive() => true;
static bool _alwaysTrue() => true;
static bool _alwaysFalse() => false;
void handle(PlayerNavigationKey navigationKey) {
if (navigationKey == PlayerNavigationKey.home) {
@@ -331,6 +335,12 @@ class PlayerNavigationCoordinator {
dismissPrompt();
return;
}
if (navigationKey == PlayerNavigationKey.back && _exitPlayerBeforeChrome()) {
// Mobile Back exits even with the chrome up (#1938); the staged
// strip/fullscreen/hide/exit chain is TV and desktop behavior.
exitPlayer();
return;
}
final disposition = resolvePlayerBackDisposition(
navigationKey: navigationKey,
contentStripVisible: chromeController.contentStripVisible,
+39
View File
@@ -570,6 +570,7 @@ void main() {
Future<bool> Function()? exitFullscreenIfActive,
bool physicalEscapeExitsFullscreen = true,
bool Function()? physicalEscapeExitsFullscreenProvider,
bool Function()? exitPlayerBeforeChrome,
VoidCallback? exitPlayer,
VoidCallback? navigateHome,
bool Function()? isActive,
@@ -581,6 +582,7 @@ void main() {
isChromePresented: isChromePresented ?? () => chromeController.controlsPresented,
exitFullscreenIfActive: exitFullscreenIfActive ?? () async => false,
physicalEscapeExitsFullscreen: physicalEscapeExitsFullscreenProvider ?? () => physicalEscapeExitsFullscreen,
exitPlayerBeforeChrome: exitPlayerBeforeChrome,
exitPlayer: exitPlayer ?? () {},
navigateHome: navigateHome ?? () {},
isActive: isActive,
@@ -635,6 +637,43 @@ void main() {
expect(exits, 1);
});
testWidgets('mobile Back exits on the first press even with the chrome up (#1938)', (tester) async {
final chromeController = PlayerChromeController();
addTearDown(chromeController.dispose);
var exits = 0;
final coordinator = coordinatorFor(
chromeController,
exitPlayerBeforeChrome: () => true,
exitPlayer: () => exits++,
);
await pumpNavigationFocus(tester, coordinator);
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);
expect(exits, 1, reason: 'a phone back must close the player even while the controls are up');
expect(chromeController.controlsVisible, isTrue, reason: 'no staged hide: the player leaves directly');
});
testWidgets('mobile Back dismisses an open prompt before exiting', (tester) async {
final chromeController = PlayerChromeController();
addTearDown(chromeController.dispose);
var promptDismissals = 0;
var exits = 0;
final coordinator = coordinatorFor(
chromeController,
isPromptOpen: () => true,
dismissPrompt: () => promptDismissals++,
exitPlayerBeforeChrome: () => true,
exitPlayer: () => exits++,
);
await pumpNavigationFocus(tester, coordinator);
await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonB);
expect(promptDismissals, 1, reason: 'prompts keep priority over the mobile exit policy');
expect(exits, 0);
});
testWidgets('Back exits on the first press when the route opened with no chrome', (tester) async {
final chromeController = PlayerChromeController(initiallyVisible: false);
addTearDown(chromeController.dispose);