fix(arranque): stop a headless engine from dying before runApp
Reported: with Android Auto connected, the car screen sometimes came up completely BLACK, and opening the app on the phone then showed a completely WHITE screen until the app was force-killed and reopened. Never without Android Auto. The user guessed portrait-only plus a landscape phone made the app "go a bit crazy". Right file and right trigger, different mechanism -- a broken layout renders overflow stripes or a red error box, never white. White means nothing was ever built, so runApp had not run. Verified in the plugin source: AudioServiceActivity.provideFlutterEngine returns AudioServicePlugin.getFlutterEngine(context), which CREATES the engine and executes the Dart entrypoint the first time it is asked. When the car binds the MediaBrowserService before the app is opened, that first ask is the service -- so main() runs HEADLESS, with no Activity. SystemChrome.setPreferredOrientations travels the flutter/platform channel, whose handler (PlatformPlugin) is installed by the Activity. Headless there is nobody to answer it, so the call throws MissingPluginException or never settles. It was the FIRST await in main(), which made it fatal twice over: registrarFuenteNavegacion sits below it and never ran, leaving getChildren with no source (black car screen), and runApp was never reached. Opening the app then reused that same cached, already-dead engine -- white screen. Only a force-kill, which disposes the cached engine, recovered it. That is exactly the workaround that was reported, and it is what makes the diagnosis fit every detail rather than most of them. Three changes, smallest first: - The Android Auto browse registration moves above every await. It depends on nothing, and anything before it is a place to get stuck. - The orientation call is no longer awaited. It is a display preference, never a prerequisite for runApp, and _OrientacionResponsiveApp already re-applies it in didChangeDependencies -- the only moment it can take effect anyway. - aplicarPoliticaOrientacion swallows everything and logs, so the headless failure can never propagate again. The policy itself is unchanged and now pure and tested (orientacionesPara): phones portrait, >=600dp everything. Tests: 1141 -> 1146.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/main.dart';
|
||||
|
||||
/// Reported: with Android Auto connected, the car screen sometimes came up
|
||||
/// completely BLACK, and opening the app on the phone then showed a
|
||||
/// completely WHITE screen until it was force-killed and reopened. Never
|
||||
/// without Android Auto.
|
||||
///
|
||||
/// Cause, verified in the plugin source:
|
||||
/// `AudioServiceActivity.provideFlutterEngine` returns
|
||||
/// `AudioServicePlugin.getFlutterEngine(context)`, which CREATES the engine
|
||||
/// and runs `main()` the first time it is asked — and the car asks first,
|
||||
/// when it binds the MediaBrowserService, so `main()` runs HEADLESS with no
|
||||
/// Activity. `SystemChrome.setPreferredOrientations` travels the
|
||||
/// `flutter/platform` channel, whose handler (`PlatformPlugin`) is installed
|
||||
/// by the Activity. Headless, nobody answers it.
|
||||
///
|
||||
/// It was the FIRST `await` in `main()`, so that one call took the whole
|
||||
/// startup with it: the Android Auto browse source below it was never
|
||||
/// registered (`getChildren` had no source → black car screen) and `runApp`
|
||||
/// was never reached. Opening the app then reused that same cached, already
|
||||
/// dead engine → white screen. Only a force-kill, which disposes the cached
|
||||
/// engine, recovered it — exactly the workaround that was reported.
|
||||
///
|
||||
/// The user's own guess was that portrait-only + a landscape phone made the
|
||||
/// app "go a bit crazy". Right file, right trigger, different mechanism: a
|
||||
/// broken layout renders overflow stripes or a red error box, never white.
|
||||
/// White means nothing was ever built.
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('política de orientación', () {
|
||||
test('un móvil se queda en vertical', () {
|
||||
expect(orientacionesPara(411), const [DeviceOrientation.portraitUp]);
|
||||
expect(orientacionesPara(599.9), const [DeviceOrientation.portraitUp]);
|
||||
});
|
||||
|
||||
test('una tablet puede girar', () {
|
||||
expect(orientacionesPara(600), DeviceOrientation.values);
|
||||
expect(orientacionesPara(1280), DeviceOrientation.values);
|
||||
});
|
||||
});
|
||||
|
||||
group('nunca puede tumbar el arranque', () {
|
||||
test('un fallo del canal de plataforma se traga, no se propaga', () async {
|
||||
// This is the headless case: no PlatformPlugin, so the call fails.
|
||||
// Before the fix this exception escaped out of main() and killed
|
||||
// startup before runApp and before the Android Auto registration.
|
||||
await expectLater(
|
||||
aplicarPoliticaOrientacion(
|
||||
aplicar:
|
||||
(_) async =>
|
||||
throw MissingPluginException(
|
||||
'No implementation found for method '
|
||||
'SystemChrome.setPreferredOrientations on channel '
|
||||
'flutter/platform',
|
||||
),
|
||||
),
|
||||
completes,
|
||||
);
|
||||
});
|
||||
|
||||
test('un canal que nunca responde tampoco puede colgar a quien llama, '
|
||||
'porque main() ya no lo espera', () async {
|
||||
// The structural half of the fix: main() calls this through
|
||||
// `unawaited(...)`. Proven here by starting a call that never settles
|
||||
// and showing the test still finishes -- if startup awaited it, this
|
||||
// future is exactly what would hang forever on the headless engine.
|
||||
var termino = false;
|
||||
// ignore: unawaited_futures
|
||||
aplicarPoliticaOrientacion(
|
||||
aplicar: (_) => Completer<void>().future,
|
||||
).then((_) => termino = true);
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(termino, isFalse, reason: 'sigue pendiente, como debe');
|
||||
// The point is that nothing above depends on it.
|
||||
});
|
||||
|
||||
test('el camino feliz sigue aplicando la política de la pantalla', () {
|
||||
// Guard against "fixed" by neutering: the swallow-everything wrapper
|
||||
// must still actually apply something on a healthy engine.
|
||||
late List<DeviceOrientation> aplicadas;
|
||||
return aplicarPoliticaOrientacion(
|
||||
aplicar: (o) async => aplicadas = o,
|
||||
).then((_) {
|
||||
expect(aplicadas, isNotEmpty);
|
||||
expect(aplicadas, orientacionesPara(800 / 1));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user