diff --git a/lib/utils/platform_detector.dart b/lib/utils/platform_detector.dart index 6bf084d1..20275c74 100644 --- a/lib/utils/platform_detector.dart +++ b/lib/utils/platform_detector.dart @@ -48,6 +48,25 @@ AndroidTvFeatureDetection detectAndroidTvFromSystemFeatures(Iterable fea ); } +/// Whether a floating player may be offered, given the host platform's own +/// picture-in-picture capability and the detected form factor. +/// +/// Cars commonly lack `FEATURE_PICTURE_IN_PICTURE`, and a floating player would +/// keep the app's UI on screen while driving, which `DD-2` forbids. TV form +/// factors have no windowed surface to float into. +/// +/// [hostSupportsPictureInPicture] is injected rather than read from [Platform] +/// so the form-factor vetoes stay observable on hosts that never support PiP: +/// on the Linux and Windows CI runners every [Platform] branch of the real gate +/// is false and unmockable, which would otherwise make the vetoes vacuous +/// exactly where the release is gated. +bool pictureInPictureAllowed({ + required bool hostSupportsPictureInPicture, + required bool isAppleTv, + required bool isTv, + required bool isAutomotive, +}) => hostSupportsPictureInPicture && !isAppleTv && !isTv && !isAutomotive; + /// Service for detecting if the app is running on Android TV or Apple TV. class TvDetectionService { static final AsyncSingleton _singleton = AsyncSingleton(); @@ -267,11 +286,12 @@ class PlatformDetector { return isAppleTV() || isDesktopOS() || (Platform.isAndroid && isTV()); } - static bool supportsPictureInPicture() { - // Cars commonly lack FEATURE_PICTURE_IN_PICTURE, and a floating player - // would keep the app's UI on screen while driving, which `DD-2` forbids. - return !isAppleTV() && !isTV() && !isAutomotive() && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); - } + static bool supportsPictureInPicture() => pictureInPictureAllowed( + hostSupportsPictureInPicture: Platform.isAndroid || Platform.isIOS || Platform.isMacOS, + isAppleTv: isAppleTV(), + isTv: isTV(), + isAutomotive: isAutomotive(), + ); /// Detects if the device is likely a tablet based on screen size /// Uses diagonal screen size to determine if device is a tablet diff --git a/test/services/settings_service_test.dart b/test/services/settings_service_test.dart index 1c4c4282..940f0d2d 100644 --- a/test/services/settings_service_test.dart +++ b/test/services/settings_service_test.dart @@ -236,7 +236,13 @@ void main() { final settings = await SettingsService.getInstance(); await settings.write(SettingsService.autoPip, true); - expect(settings.read(SettingsService.autoPip), isTrue); + // The pref can only surface a stored true where the host itself supports + // PiP. That term is Platform.isAndroid/isIOS/isMacOS, unmockable and false + // on the Linux and Windows CI hosts, where the gate pins the pref off no + // matter what is stored. pictureInPictureAllowed covers the automotive + // veto itself on every host. + final hostSupportsPip = PlatformDetector.supportsPictureInPicture(); + expect(settings.read(SettingsService.autoPip), hostSupportsPip ? isTrue : isFalse); TvDetectionService.debugSetAutomotiveOverride(true); diff --git a/test/utils/platform_detector_test.dart b/test/utils/platform_detector_test.dart index 7624c2c5..a89255f3 100644 --- a/test/utils/platform_detector_test.dart +++ b/test/utils/platform_detector_test.dart @@ -113,4 +113,32 @@ void main() { expect(detection.isAutomotive, isFalse); }); }); + + group('pictureInPictureAllowed', () { + bool allowed({bool host = true, bool appleTv = false, bool tv = false, bool automotive = false}) => + pictureInPictureAllowed( + hostSupportsPictureInPicture: host, + isAppleTv: appleTv, + isTv: tv, + isAutomotive: automotive, + ); + + test('a plain handheld host may float a player', () { + expect(allowed(), isTrue); + }); + + test('automotive vetoes a host that otherwise supports PiP', () { + expect(allowed(automotive: true), isFalse); + }); + + test('TV form factors veto a host that otherwise supports PiP', () { + expect(allowed(tv: true), isFalse); + expect(allowed(appleTv: true), isFalse); + }); + + test('a host without PiP is never allowed, whatever the form factor', () { + expect(allowed(host: false), isFalse); + expect(allowed(host: false, automotive: true), isFalse); + }); + }); }