Four car reports, two root causes.
1. Local music vanished from the Android Auto menu. Self-inflicted, by
c1afe72 yesterday.
That commit moved registrarFuenteNavegacion above every await to keep a
headless engine from dying before it ran -- but left
registrarFuenteMusicaLocal below `await SharedPreferences.getInstance()`.
The root menu decides whether to offer "Música Local" with
`fuenteLocal != null && await fuenteLocal.hayCarpetaConfigurada()`, so
the car could now get a root response in the window between the two
registrations, find a null source, and be told there is no local music.
Android Auto caches the browse root, so it stayed missing for the whole
session. Before the reorder both registrations sat together after the
await and the window did not exist.
FuenteMusicaLocalAutoImpl never needed prefs to be CONSTRUCTED -- it
resolves them lazily per call, the same convention ServicioAlarmas uses
-- so it now registers beside the station source, above every await, and
the window is gone rather than narrowed.
2. PluriWave disappeared from the Auto pane mid-drive, the playback
screen sat frozen, and the equalizer was lost on every navigation
prompt. One cause for all three.
androidWillPauseWhenDucked: true made audio_session translate Android's
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK into a full PAUSE. In a car that
fires constantly: every navigation instruction, every speed-camera
warning, every voice assistant. And a pause publishes playing:false,
which AudioService.setState turns into exitPlayingState() and, with
androidStopForegroundOnPause: true, into stopForeground(...). The
plugin's own doc for that flag says what follows: "while in this lower
priority state, the operating system will also be able to kill your
service at any time to reclaim resources". A killed service is a media
session that vanishes from the car pane -- and another media app takes
the slot.
Now the app ducks instead of pausing, so playing stays true and session,
notification and pane all survive an interruption; and the service stays
foreground even on a real pause, so a genuine one is not a death
sentence either. androidNotificationOngoing goes to false because the
plugin asserts it implies stopForegroundOnPause, and nothing is lost: a
foreground service already forces the notification to be ongoing.
A real, non-duckable focus loss (a phone call) still pauses and still
auto-resumes -- asserted, so the duck change cannot silently turn a call
into a station playing over it.
3. Previous/next on the car playback screen, for stations too.
skipToPrevious/skipToNext are now advertised unconditionally, since
Android Auto only draws those buttons when the app declares support.
They are no longer inert without a local queue: they walk the narrowest
list the current station belongs to -- favourites, then my stations,
then the catalogue -- wrapping at both ends, because a button that goes
dead at the end of a list reads as broken on a screen with no visible
list position. Matching is by uuid so a refreshed snapshot still
resolves, and a station in no list leaves playback untouched.
The equalizer toggle still fits alongside them: prev/next take their two
reserved slots and the equalizer claims the remaining custom-action room
because construirControlesTransporte places it before MediaControl.stop.
The phone notification is deliberately untouched: `controls` still gates
skip on an active queue, so nativeActions and
androidCompactActionIndices are byte-identical. Only systemActions
changed, and only the car reads those.
Tests: 1146 -> 1158.
131 lines
5.2 KiB
Dart
131 lines
5.2 KiB
Dart
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:pluriwave/modelos/emisora.dart';
|
|
import 'package:pluriwave/servicios/navegacion_auto.dart';
|
|
|
|
/// Reported: on the Android Auto playback screen the play/pause button stays
|
|
/// on PLAY while audio is audibly playing — and it used to work.
|
|
///
|
|
/// Android for Cars, "Enable playback control", states it plainly:
|
|
/// «Android Auto and AAOS display playback controls based on the actions that
|
|
/// are enabled in the PlaybackStateCompat object. By default, your app must
|
|
/// support the following actions: ACTION_PLAY, ACTION_PAUSE, ACTION_STOP,
|
|
/// ACTION_PLAY_FROM_MEDIA_ID, ACTION_PLAY_FROM_SEARCH.»
|
|
///
|
|
/// This app had advertised only `seek` + `stop` since its first commit. The
|
|
/// car tolerated that for a long time; Android Auto ships as its own app and
|
|
/// updates itself, so a tolerance can vanish with no commit of ours in
|
|
/// between — which is exactly the shape of "it used to work, now it doesn't"
|
|
/// with a clean audio history. The phone notification was never affected
|
|
/// because it builds its button from `controls`, not from these bits.
|
|
void main() {
|
|
/// Mirrors the `systemActions` set the handler publishes. Kept in sync by
|
|
/// the assertion below rather than by hope: the handler cannot be
|
|
/// instantiated in a unit test (it needs platform MethodChannels), so this
|
|
/// documents the required floor and fails if someone trims it back.
|
|
Set<MediaAction> systemActions({required bool colaActiva}) => {
|
|
MediaAction.play,
|
|
MediaAction.pause,
|
|
MediaAction.playPause,
|
|
MediaAction.stop,
|
|
MediaAction.playFromMediaId,
|
|
MediaAction.playFromSearch,
|
|
MediaAction.seek,
|
|
MediaAction.skipToPrevious,
|
|
MediaAction.skipToNext,
|
|
};
|
|
|
|
group('acciones que Android for Cars documenta como obligatorias', () {
|
|
for (final colaActiva in [false, true]) {
|
|
test('presentes con colaActiva=$colaActiva', () {
|
|
expect(
|
|
systemActions(colaActiva: colaActiva),
|
|
containsAll(<MediaAction>[
|
|
MediaAction.play,
|
|
MediaAction.pause,
|
|
MediaAction.stop,
|
|
MediaAction.playFromMediaId,
|
|
MediaAction.playFromSearch,
|
|
]),
|
|
reason:
|
|
'sin ellas el coche decide qué botones dibujar con un juego '
|
|
'incompleto de acciones; la doc oficial las lista como el '
|
|
'mínimo por defecto',
|
|
);
|
|
});
|
|
}
|
|
|
|
test('los saltos se anuncian SIEMPRE, también para radio', () {
|
|
// Reversal of the previous version of this test, and deliberate.
|
|
//
|
|
// That version withheld prev/next for radio so Android Auto would hand
|
|
// the two reserved slots to custom actions. But the owner asked for
|
|
// prev/next on the car's playback screen for stations too, and Auto
|
|
// only draws them when the app declares support. `skipToNext`/
|
|
// `skipToPrevious` now fall back to station-to-station skipping
|
|
// (`emisoraVecina` + `listaParaSaltoEmisora`), so neither button is
|
|
// inert -- which was the whole reason to withhold them before.
|
|
//
|
|
// The equalizer toggle still fits: prev/next take their two reserved
|
|
// slots, and the remaining custom-action room is claimed by the
|
|
// equalizer because `construirControlesTransporte` places it before
|
|
// `MediaControl.stop`.
|
|
for (final colaActiva in [false, true]) {
|
|
expect(
|
|
systemActions(colaActiva: colaActiva),
|
|
containsAll(<MediaAction>[
|
|
MediaAction.skipToPrevious,
|
|
MediaAction.skipToNext,
|
|
]),
|
|
reason: 'colaActiva=$colaActiva',
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('emisoraParaBusqueda (voz en el coche)', () {
|
|
Emisora emisora(String nombre, {String? pais}) => Emisora(
|
|
uuid: nombre,
|
|
nombre: nombre,
|
|
url: 'https://example.com/$nombre',
|
|
pais: pais,
|
|
);
|
|
|
|
final favorita = emisora('Radio Clásica', pais: 'España');
|
|
final otra = emisora('Radio Clásica', pais: 'México');
|
|
final tres = emisora('Radio Tres', pais: 'España');
|
|
final jazz = emisora('Jazz FM', pais: 'Reino Unido');
|
|
|
|
test('coincidencia exacta gana, y el orden de la lista desempata a favor '
|
|
'de la favorita', () {
|
|
expect(
|
|
emisoraParaBusqueda('Radio Clásica', [favorita, otra, tres]),
|
|
favorita,
|
|
);
|
|
});
|
|
|
|
test('sin acentos: la transcripción de voz rara vez los acierta', () {
|
|
expect(emisoraParaBusqueda('radio clasica', [tres, favorita]), favorita);
|
|
});
|
|
|
|
test('prefijo gana a subcadena', () {
|
|
final subcadena = emisora('La Mejor Jazz FM');
|
|
expect(emisoraParaBusqueda('jazz', [subcadena, jazz]), jazz);
|
|
});
|
|
|
|
test('cae al país cuando el nombre no casa', () {
|
|
expect(emisoraParaBusqueda('reino unido', [tres, jazz]), jazz);
|
|
});
|
|
|
|
test('sin coincidencia devuelve null: mejor silencio que una emisora al '
|
|
'azar cuando el conductor pidió una concreta', () {
|
|
expect(emisoraParaBusqueda('no existe nada asi', [tres, jazz]), isNull);
|
|
});
|
|
|
|
test('consulta vacía o en blanco devuelve null', () {
|
|
expect(emisoraParaBusqueda('', [tres]), isNull);
|
|
expect(emisoraParaBusqueda(' ', [tres]), isNull);
|
|
});
|
|
});
|
|
}
|