Files
plezy/lib/services/driver_distraction.dart
T
edde746 7ce5a443fd feat(automotive): read the vehicle's driver-distraction state
Android Automotive tells an app when the car requires distraction optimization,
and Plezy never asked. A monitor now watches CarUxRestrictions and publishes the
verdict over the existing platform channel, where a single Dart gate answers
whether playback may start.

The car service is reached through the lifecycle-listener overload rather than
Car.createCar(Context). That overload blocks its caller for up to five seconds
polling ServiceManager, and on car-service death it reaches killClient(), which
kills the hosting process for any context that is not an Activity or a Service -
a crash in a system component would take the app down with it. Head units on
Android 9 and 10 predate the listener, so a legacy ServiceConnection is used
there, with the same identity guard on reconnect.

A vehicle that has not answered yet counts as restricted, and one deadline is
spent resolving it rather than one per request, so a wedged car service delays
playback once instead of on every open.
2026-08-06 03:45:09 +02:00

57 lines
2.7 KiB
Dart

import 'package:flutter/widgets.dart';
import '../utils/platform_detector.dart';
import 'car_ux_restrictions_service.dart';
/// Android Automotive OS driver-distraction gating for Plezy's `video` app
/// category (car app quality `DD-2` / `DD-3`).
///
/// Two obligations follow from `DD-2`, and this single predicate serves both:
/// audio must stop when driving starts, and it must not be resumable while
/// driving. The second obligation covers every path that can start audio, not
/// just OS media-session commands — a gapless track transition or queue
/// auto-advance landing just after driving starts must fail closed too.
///
/// Authority is the vehicle's own user-experience restrictions on the driver
/// display ([CarUxRestrictionsService]), which is the mechanism the platform
/// documents for apps that must separate "driving" from "not in the
/// foreground". Lifecycle state cannot make that distinction: the app is
/// equally not-resumed when the system covers it for driving and when a parked
/// driver opens navigation, so deriving authority from lifecycle alone silenced
/// parked background audio — something `DD-2` never asked for.
///
/// Where the vehicle cannot answer ([CarUxRestrictionState.unknown] — an older
/// head unit, a car service that failed to connect, or a driver display that
/// could not be resolved) the previous lifecycle rule still applies, including
/// its fail-closed treatment of a null state: a command arriving before the
/// first lifecycle message is denied, and nothing is playing that early, so the
/// strictness costs nothing.
bool automotivePlaybackAllowed({
required bool isAutomotive,
required AppLifecycleState? state,
CarUxRestrictionState restrictions = CarUxRestrictionState.unknown,
}) {
if (!isAutomotive) return true;
return switch (restrictions) {
CarUxRestrictionState.restricted => false,
CarUxRestrictionState.unrestricted => true,
CarUxRestrictionState.unknown => state == AppLifecycleState.resumed,
};
}
/// [automotivePlaybackAllowed] against the ambient form factor, vehicle state
/// and lifecycle, for owners that hold no injected state of their own.
///
/// Short-circuits before reading [WidgetsBinding.instance] so this stays usable
/// from plain `test()` suites, where the binding is not initialized and the
/// `instance` getter throws.
bool automotivePlaybackAllowedNow() {
if (!PlatformDetector.isAutomotive()) return true;
final restrictions = CarUxRestrictionsService.instance.state;
return automotivePlaybackAllowed(
isAutomotive: true,
state: restrictions == CarUxRestrictionState.unknown ? WidgetsBinding.instance.lifecycleState : null,
restrictions: restrictions,
);
}