test(automotive): assert the picture-in-picture vetoes on every host

41ffaa7f2 gated picture-in-picture on FEATURE_AUTOMOTIVE and added a
settings case for it, but the assertion that case leads with — a stored
auto-PiP true surviving a read — needs supportsPictureInPicture() to be
true, and that gate ends in Platform.isAndroid || isIOS || isMacOS. The
term is false and unmockable on the Linux and Windows runners, so the
case passed on a macOS host and could never pass in CI: sanity checks
have been red for six commits on this one failure out of 4723. f13f5af6e
recorded it as a pre-existing Windows-host failure, but it entered in
this window and is red on Linux too.

Extract the gate's decision into a pure pictureInPictureAllowed that
takes the host's own capability as a parameter, the way
driver_distraction.dart already splits automotivePlaybackAllowed from its
ambient wrapper. The boolean algebra is unchanged, so the three callers
keep their behaviour; what changes is that the automotive and TV vetoes
become observable where every Platform branch is false, instead of being
vacuous on the host that gates the release.

The settings case keeps the pref-level contract on both host classes: a
stored true survives where the host supports PiP, and the gate pins it
off where it does not.

Verified with the host term forced false to emulate a Linux runner: both
files stay green, as does the full suite on macOS.
This commit is contained in:
edde746
2026-07-30 03:16:49 +02:00
parent 0a6865fa18
commit 88ffe0806d
3 changed files with 60 additions and 6 deletions
+25 -5
View File
@@ -48,6 +48,25 @@ AndroidTvFeatureDetection detectAndroidTvFromSystemFeatures(Iterable<String> 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. /// Service for detecting if the app is running on Android TV or Apple TV.
class TvDetectionService { class TvDetectionService {
static final AsyncSingleton<TvDetectionService> _singleton = AsyncSingleton(); static final AsyncSingleton<TvDetectionService> _singleton = AsyncSingleton();
@@ -267,11 +286,12 @@ class PlatformDetector {
return isAppleTV() || isDesktopOS() || (Platform.isAndroid && isTV()); return isAppleTV() || isDesktopOS() || (Platform.isAndroid && isTV());
} }
static bool supportsPictureInPicture() { static bool supportsPictureInPicture() => pictureInPictureAllowed(
// Cars commonly lack FEATURE_PICTURE_IN_PICTURE, and a floating player hostSupportsPictureInPicture: Platform.isAndroid || Platform.isIOS || Platform.isMacOS,
// would keep the app's UI on screen while driving, which `DD-2` forbids. isAppleTv: isAppleTV(),
return !isAppleTV() && !isTV() && !isAutomotive() && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS); isTv: isTV(),
} isAutomotive: isAutomotive(),
);
/// Detects if the device is likely a tablet based on screen size /// Detects if the device is likely a tablet based on screen size
/// Uses diagonal screen size to determine if device is a tablet /// Uses diagonal screen size to determine if device is a tablet
+7 -1
View File
@@ -236,7 +236,13 @@ void main() {
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
await settings.write(SettingsService.autoPip, true); 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); TvDetectionService.debugSetAutomotiveOverride(true);
+28
View File
@@ -113,4 +113,32 @@ void main() {
expect(detection.isAutomotive, isFalse); 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);
});
});
} }