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:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user