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.
123 lines
4.9 KiB
Dart
123 lines
4.9 KiB
Dart
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/services/car_ux_restrictions_service.dart';
|
|
import 'package:plezy/services/driver_distraction.dart';
|
|
import 'package:plezy/utils/platform_detector.dart';
|
|
|
|
void main() {
|
|
group('automotivePlaybackAllowed', () {
|
|
test('never restricts playback off Android Automotive OS', () {
|
|
for (final state in [...AppLifecycleState.values, null]) {
|
|
expect(
|
|
automotivePlaybackAllowed(isAutomotive: false, state: state),
|
|
isTrue,
|
|
reason: 'non-automotive playback must not depend on lifecycle state ($state)',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('allows playback on a car only while the app is resumed', () {
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: AppLifecycleState.resumed), isTrue);
|
|
});
|
|
|
|
test('stops playback on the onPause edge that every car delivers', () {
|
|
// Android `onPause` maps to `inactive`, and cars without the Automotive
|
|
// compatibility mode never go on to deliver `onStop`. This is the edge
|
|
// the rejected build ignored.
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: AppLifecycleState.inactive), isFalse);
|
|
});
|
|
|
|
test('stops playback on the onStop edges compatibility-mode cars deliver', () {
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: AppLifecycleState.hidden), isFalse);
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: AppLifecycleState.paused), isFalse);
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: AppLifecycleState.detached), isFalse);
|
|
});
|
|
|
|
test('fails closed on an unknown lifecycle state', () {
|
|
expect(automotivePlaybackAllowed(isAutomotive: true, state: null), isFalse);
|
|
});
|
|
|
|
test('a vehicle reporting restrictions overrides lifecycle in both directions', () {
|
|
// The point of the platform signal: parked-but-backgrounded keeps playing,
|
|
// and driving stops audio even while the app still looks resumed.
|
|
for (final state in [...AppLifecycleState.values, null]) {
|
|
expect(
|
|
automotivePlaybackAllowed(isAutomotive: true, state: state, restrictions: CarUxRestrictionState.unrestricted),
|
|
isTrue,
|
|
reason: 'a parked car must allow playback regardless of lifecycle ($state)',
|
|
);
|
|
expect(
|
|
automotivePlaybackAllowed(isAutomotive: true, state: state, restrictions: CarUxRestrictionState.restricted),
|
|
isFalse,
|
|
reason: 'a driving car must deny playback regardless of lifecycle ($state)',
|
|
);
|
|
}
|
|
});
|
|
|
|
test('an unknown vehicle keeps the lifecycle rule', () {
|
|
expect(
|
|
automotivePlaybackAllowed(
|
|
isAutomotive: true,
|
|
state: AppLifecycleState.resumed,
|
|
restrictions: CarUxRestrictionState.unknown,
|
|
),
|
|
isTrue,
|
|
);
|
|
expect(
|
|
automotivePlaybackAllowed(
|
|
isAutomotive: true,
|
|
state: AppLifecycleState.inactive,
|
|
restrictions: CarUxRestrictionState.unknown,
|
|
),
|
|
isFalse,
|
|
);
|
|
});
|
|
|
|
test('a restricted vehicle never unlocks playback off a car', () {
|
|
expect(
|
|
automotivePlaybackAllowed(
|
|
isAutomotive: false,
|
|
state: AppLifecycleState.resumed,
|
|
restrictions: CarUxRestrictionState.restricted,
|
|
),
|
|
isTrue,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('automotivePlaybackAllowedNow', () {
|
|
setUp(() {
|
|
TvDetectionService.debugReset();
|
|
addTearDown(TvDetectionService.debugReset);
|
|
addTearDown(() => CarUxRestrictionsService.debugSetOverride(null));
|
|
});
|
|
|
|
testWidgets('reads the ambient form factor and lifecycle', (tester) async {
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
|
expect(automotivePlaybackAllowedNow(), isTrue);
|
|
|
|
TvDetectionService.debugSetAutomotiveOverride(true);
|
|
expect(automotivePlaybackAllowedNow(), isTrue);
|
|
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
|
expect(automotivePlaybackAllowedNow(), isFalse);
|
|
|
|
TvDetectionService.debugSetAutomotiveOverride(false);
|
|
expect(automotivePlaybackAllowedNow(), isTrue);
|
|
});
|
|
|
|
testWidgets('a parked vehicle keeps playback alive while the app is backgrounded', (tester) async {
|
|
TvDetectionService.debugSetAutomotiveOverride(true);
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
|
|
expect(automotivePlaybackAllowedNow(), isFalse, reason: 'no vehicle signal yet: lifecycle still rules');
|
|
|
|
CarUxRestrictionsService.debugSetOverride(CarUxRestrictionState.unrestricted);
|
|
expect(automotivePlaybackAllowedNow(), isTrue);
|
|
|
|
CarUxRestrictionsService.debugSetOverride(CarUxRestrictionState.restricted);
|
|
tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
|
|
expect(automotivePlaybackAllowedNow(), isFalse, reason: 'driving denies playback even while resumed');
|
|
});
|
|
});
|
|
}
|