Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
490ae29bd4 | ||
|
|
7d86ddfae9 | ||
|
|
3398d02a43 |
+44
-9
@@ -26,8 +26,30 @@ const androidNotificationIconResource = 'drawable/ic_stat_pluriwave';
|
||||
const configuracionAudioService = AudioServiceConfig(
|
||||
androidNotificationChannelId: 'es.freetimelab.pluriwave.audio',
|
||||
androidNotificationChannelName: 'PluriWave Radio',
|
||||
androidNotificationOngoing: true,
|
||||
androidStopForegroundOnPause: true,
|
||||
// Paired with `androidStopForegroundOnPause: false` below, and required to
|
||||
// be: the plugin asserts `androidNotificationOngoing` implies
|
||||
// `androidStopForegroundOnPause`. Nothing is lost by turning it off —
|
||||
// while the service is in the foreground the OS forces the notification to
|
||||
// be ongoing anyway, which is now the whole time playback is alive.
|
||||
androidNotificationOngoing: false,
|
||||
// The service stays in the FOREGROUND while paused.
|
||||
//
|
||||
// With `true`, a pause called `stopForeground(...)`, and a service that is
|
||||
// not in the foreground is a service Android may kill at will. In the car
|
||||
// that is exactly what happened: an interruption paused playback, the
|
||||
// service dropped out of the foreground, Android reclaimed it, and
|
||||
// PluriWave disappeared from the Android Auto pane — another media app
|
||||
// took the slot. Ducking (see `ServicioAudioSession.configurar`) removes
|
||||
// most pauses, but a real pause must not be a death sentence either.
|
||||
//
|
||||
// The plugin's own doc for this flag says it outright: «while in this
|
||||
// lower priority state, the operating system will also be able to kill
|
||||
// your service at any time to reclaim resources».
|
||||
//
|
||||
// Cost of `false`: the notification is not swipe-dismissible while paused,
|
||||
// only after Stop. That is how every serious media app behaves, and Stop
|
||||
// still tears everything down.
|
||||
androidStopForegroundOnPause: false,
|
||||
notificationColor: PluriWaveTokens.brand,
|
||||
androidNotificationIcon: androidNotificationIconResource,
|
||||
);
|
||||
@@ -56,6 +78,26 @@ Future<void> main() async {
|
||||
final fuenteAuto = FuenteEmisorasAutoLocal();
|
||||
registrarFuenteNavegacion(fuenteAuto);
|
||||
|
||||
// Local music registers HERE, above every await, alongside the station
|
||||
// source — not after `SharedPreferences.getInstance()` where it used to
|
||||
// sit.
|
||||
//
|
||||
// Regression this fixes, self-inflicted by the reordering above: the root
|
||||
// menu decides whether to offer "Música Local" with
|
||||
// `fuenteLocal != null && await fuenteLocal.hayCarpetaConfigurada()`.
|
||||
// Moving ONLY the station source above the awaits meant the car could get
|
||||
// a root response in the window before this line ran, find a null source,
|
||||
// and be told there is no local music — and Android Auto caches the browse
|
||||
// root, so it stayed missing for the whole session. Before the reorder
|
||||
// both registrations sat together after the await, so the window did not
|
||||
// exist.
|
||||
//
|
||||
// `FuenteMusicaLocalAutoImpl` needs no prefs to be CONSTRUCTED: it
|
||||
// resolves them lazily per call (`_resolverPrefs`, falling back to
|
||||
// `getInstance()`), the same convention `ServicioAlarmas` uses. So there
|
||||
// was never a reason for it to wait on that await.
|
||||
registrarFuenteMusicaLocal(FuenteMusicaLocalAutoImpl());
|
||||
|
||||
// Cosmetic, and deliberately NOT awaited: a display preference must never
|
||||
// gate `runApp`. `_OrientacionResponsiveApp.didChangeDependencies` applies
|
||||
// it again as soon as a real view exists, which is the only moment it can
|
||||
@@ -66,13 +108,6 @@ Future<void> main() async {
|
||||
// injected into every state/service below.
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Local-music browse source (Design "getChildren data source
|
||||
// registration"), same injectable-prefs DI convention as every other
|
||||
// startup service — required so `_fuenteMusicaLocalGlobal` is ever
|
||||
// non-null; without this registration the local-music root would stay
|
||||
// permanently hidden (`hayCarpetaConfigurada()` has no fuente to ask).
|
||||
registrarFuenteMusicaLocal(FuenteMusicaLocalAutoImpl(prefs: prefs));
|
||||
|
||||
// User-saved EQ presets for the car's Ecualizador folder, same
|
||||
// injectable-prefs DI convention and same pre-init placement as the two
|
||||
// registrations above (neither depends on the AudioHandler). Passed as a
|
||||
|
||||
@@ -936,6 +936,58 @@ Future<void> reproducirPorMediaId(
|
||||
await reproducir(item);
|
||||
}
|
||||
|
||||
/// Which list previous/next should walk for [actual]: the NARROWEST list the
|
||||
/// station actually belongs to, favourites first, then my stations, then the
|
||||
/// full catalogue.
|
||||
///
|
||||
/// Narrowest-first is the point. "Next station" while playing a favourite
|
||||
/// should land on the next favourite, not on entry 4,318 of a 50,000-station
|
||||
/// catalogue that happens to sit beside it alphabetically. Falling through to
|
||||
/// [todas] only when the station is in neither curated list keeps the button
|
||||
/// working for a station reached by search.
|
||||
///
|
||||
/// Returns an empty list when [actual] is in none of them, which
|
||||
/// [emisoraVecina] turns into "do nothing".
|
||||
List<Emisora> listaParaSaltoEmisora({
|
||||
required Emisora actual,
|
||||
required List<Emisora> favoritos,
|
||||
required List<Emisora> misEmisoras,
|
||||
required List<Emisora> todas,
|
||||
}) {
|
||||
bool contiene(List<Emisora> lista) => lista.any((e) => e.uuid == actual.uuid);
|
||||
if (contiene(favoritos)) return favoritos;
|
||||
if (contiene(misEmisoras)) return misEmisoras;
|
||||
if (contiene(todas)) return todas;
|
||||
return const [];
|
||||
}
|
||||
|
||||
/// The station before or after [actual] in [lista], wrapping around at both
|
||||
/// ends.
|
||||
///
|
||||
/// Wrapping is deliberate: on a car's transport row a button that goes dead
|
||||
/// at the end of a list reads as a broken app, and there is no visible list
|
||||
/// position to explain it. Matching is by `uuid`, the same identity the
|
||||
/// browse tree uses, so a refreshed snapshot with different object instances
|
||||
/// still resolves.
|
||||
///
|
||||
/// Returns `null` when [lista] has fewer than two entries, or when [actual]
|
||||
/// is not in it — the caller must then leave playback alone rather than jump
|
||||
/// somewhere arbitrary.
|
||||
Emisora? emisoraVecina(
|
||||
Emisora? actual,
|
||||
List<Emisora> lista, {
|
||||
required bool haciaAtras,
|
||||
}) {
|
||||
if (actual == null || lista.length < 2) return null;
|
||||
final indice = lista.indexWhere((e) => e.uuid == actual.uuid);
|
||||
if (indice < 0) return null;
|
||||
final destino =
|
||||
haciaAtras
|
||||
? (indice - 1 + lista.length) % lista.length
|
||||
: (indice + 1) % lista.length;
|
||||
return lista[destino];
|
||||
}
|
||||
|
||||
/// Picks the station a spoken query refers to ("pon Radio Clásica"), over the
|
||||
/// stations the car can already browse.
|
||||
///
|
||||
|
||||
@@ -735,8 +735,14 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
MediaAction.playFromMediaId,
|
||||
MediaAction.playFromSearch,
|
||||
MediaAction.seek,
|
||||
if (colaActiva) MediaAction.skipToPrevious,
|
||||
if (colaActiva) MediaAction.skipToNext,
|
||||
// Previous/next are advertised ALWAYS now, not only for a local
|
||||
// queue. Android Auto reserves those two slots and only hands the
|
||||
// space to custom actions when the app declares no support, so
|
||||
// this is what puts prev/next on the car's transport row -- and
|
||||
// `skipToNext`/`skipToPrevious` fall back to station-to-station
|
||||
// skipping when there is no queue, so neither button is inert.
|
||||
MediaAction.skipToPrevious,
|
||||
MediaAction.skipToNext,
|
||||
},
|
||||
androidCompactActionIndices: [colaActiva ? 1 : 0],
|
||||
processingState: mapearEstadoProceso(
|
||||
@@ -1485,13 +1491,19 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
Future<void> seek(Duration position) => _player.seek(position);
|
||||
|
||||
/// Moves to the next queued track (Design ADR-3/ADR-4, Phase 3 task
|
||||
/// 3.6): a no-op when no local queue is active. Past the last track,
|
||||
/// clears the queue and stops — mirroring auto-advance's end-of-queue
|
||||
/// behavior (no wraparound).
|
||||
/// 3.6). Past the last track, clears the queue and stops — mirroring
|
||||
/// auto-advance's end-of-queue behavior (no wraparound).
|
||||
///
|
||||
/// With NO local queue this now moves to the next STATION instead of doing
|
||||
/// nothing: the car's transport row offers previous/next for radio too,
|
||||
/// and a button that is present but inert is worse than no button.
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
final cola = _colaLocal;
|
||||
if (cola == null) return;
|
||||
if (cola == null) {
|
||||
await _saltarEmisora(haciaAtras: false);
|
||||
return;
|
||||
}
|
||||
final siguiente = cola.conSiguiente();
|
||||
if (siguiente == null) {
|
||||
_desactivarCola();
|
||||
@@ -1502,18 +1514,53 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
||||
await _reproducirEntradaCola(siguiente.actual);
|
||||
}
|
||||
|
||||
/// Moves to the previous queued track (Design ADR-4, Phase 3 task 3.6):
|
||||
/// a no-op when no local queue is active. Clamps at the first track
|
||||
/// (restarts it) instead of wrapping to the last one.
|
||||
/// Moves to the previous queued track (Design ADR-4, Phase 3 task 3.6).
|
||||
/// Clamps at the first track (restarts it) instead of wrapping to the last
|
||||
/// one. With no local queue, moves to the previous STATION — see
|
||||
/// [skipToNext].
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
final cola = _colaLocal;
|
||||
if (cola == null) return;
|
||||
if (cola == null) {
|
||||
await _saltarEmisora(haciaAtras: true);
|
||||
return;
|
||||
}
|
||||
final anterior = cola.conAnterior();
|
||||
_colaLocal = anterior;
|
||||
await _reproducirEntradaCola(anterior.actual);
|
||||
}
|
||||
|
||||
/// Station-to-station skipping for the car's transport row.
|
||||
///
|
||||
/// The list to walk is resolved by [listaParaSaltoEmisora]: the narrowest
|
||||
/// list the current station actually belongs to, favourites first. Anything
|
||||
/// unresolvable — no source, no current station, a station that is in no
|
||||
/// list, a single-entry list — leaves playback untouched. Never throws;
|
||||
/// this runs from a hardware/steering-wheel button and an exception here
|
||||
/// would surface as the app going silent mid-drive.
|
||||
Future<void> _saltarEmisora({required bool haciaAtras}) async {
|
||||
try {
|
||||
final fuente = _fuenteNavegacionGlobal;
|
||||
final actual = emisoraActual;
|
||||
if (fuente == null || actual == null) return;
|
||||
final lista = listaParaSaltoEmisora(
|
||||
actual: actual,
|
||||
favoritos: await fuente.favoritos(),
|
||||
misEmisoras: await fuente.misEmisoras(),
|
||||
todas: await fuente.todas(),
|
||||
);
|
||||
final destino = emisoraVecina(actual, lista, haciaAtras: haciaAtras);
|
||||
if (destino == null) return;
|
||||
await playMediaItem(mediaItemParaEmisora(destino, l10n: _textos));
|
||||
} catch (e) {
|
||||
developer.log(
|
||||
'[PluriWave] Error saltando de emisora: $e',
|
||||
name: 'ServicioAudio',
|
||||
level: 900,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches the equalizer's only custom action (decision
|
||||
/// `auto/ecualizador-diseno`): `accionEqToggle` flips on/off, delegating
|
||||
/// to the existing [setEcualizadorActivo] — the SAME entry point the
|
||||
|
||||
@@ -58,9 +58,26 @@ class ServicioAudioSession {
|
||||
Future<void> configurar() async {
|
||||
try {
|
||||
final sesion = await _obtenerSesion();
|
||||
// DUCK, never pause, when another app asks for transient focus.
|
||||
//
|
||||
// `androidWillPauseWhenDucked: true` makes `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 "OK Google" — and each one used to stop
|
||||
// the radio outright instead of dipping the volume for two seconds.
|
||||
//
|
||||
// Worse than the audio gap: a pause publishes `playing: false`, which
|
||||
// `AudioService.setState` turns into `exitPlayingState()` and, with
|
||||
// `androidStopForegroundOnPause`, into `stopForeground(...)`. A service
|
||||
// that is no longer in the foreground is killable, and when Android
|
||||
// took it the app vanished from the Android Auto pane mid-drive and
|
||||
// another media app took its slot. Ducking keeps `playing: true`
|
||||
// throughout, so the session, the notification and the car pane all
|
||||
// survive an interruption — which is also what keeps the equalizer
|
||||
// alive across it.
|
||||
await sesion.configure(
|
||||
const AudioSessionConfiguration.music().copyWith(
|
||||
androidWillPauseWhenDucked: true,
|
||||
androidWillPauseWhenDucked: false,
|
||||
),
|
||||
);
|
||||
await _interrupcionesSub?.cancel();
|
||||
|
||||
@@ -70,10 +70,11 @@ void main() {
|
||||
// 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);
|
||||
unawaited(
|
||||
aplicarPoliticaOrientacion(
|
||||
aplicar: (_) => Completer<void>().future,
|
||||
).then((_) => termino = true),
|
||||
);
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(termino, isFalse, reason: 'sigue pendiente, como debe');
|
||||
|
||||
@@ -31,8 +31,8 @@ void main() {
|
||||
MediaAction.playFromMediaId,
|
||||
MediaAction.playFromSearch,
|
||||
MediaAction.seek,
|
||||
if (colaActiva) MediaAction.skipToPrevious,
|
||||
if (colaActiva) MediaAction.skipToNext,
|
||||
MediaAction.skipToPrevious,
|
||||
MediaAction.skipToNext,
|
||||
};
|
||||
|
||||
group('acciones que Android for Cars documenta como obligatorias', () {
|
||||
@@ -55,22 +55,31 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
test('los saltos SOLO se anuncian con cola activa', () {
|
||||
// Android Auto reserves the prev/next slots for these actions and, when
|
||||
// the app does not support them, hands the space to custom actions --
|
||||
// which is where the equalizer toggle lives. Advertising skips for
|
||||
// radio would take that space away for buttons that do nothing.
|
||||
expect(
|
||||
systemActions(colaActiva: false),
|
||||
isNot(contains(MediaAction.skipToPrevious)),
|
||||
);
|
||||
expect(
|
||||
systemActions(colaActiva: true),
|
||||
containsAll(<MediaAction>[
|
||||
MediaAction.skipToPrevious,
|
||||
MediaAction.skipToNext,
|
||||
]),
|
||||
);
|
||||
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',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/modelos/emisora.dart';
|
||||
import 'package:pluriwave/servicios/navegacion_auto.dart';
|
||||
|
||||
/// Requested: the Android Auto playback screen must offer previous/next for
|
||||
/// stations too, not only for a local-music queue. Those buttons appear
|
||||
/// because `skipToPrevious`/`skipToNext` are advertised in `systemActions` —
|
||||
/// so they have to actually move, or the car shows two dead buttons.
|
||||
void main() {
|
||||
Emisora emisora(String uuid) =>
|
||||
Emisora(uuid: uuid, nombre: uuid, url: 'https://example.com/$uuid');
|
||||
|
||||
final a = emisora('a');
|
||||
final b = emisora('b');
|
||||
final c = emisora('c');
|
||||
|
||||
group('emisoraVecina', () {
|
||||
test('avanza y retrocede dentro de la lista', () {
|
||||
expect(emisoraVecina(a, [a, b, c], haciaAtras: false), b);
|
||||
expect(emisoraVecina(b, [a, b, c], haciaAtras: true), a);
|
||||
});
|
||||
|
||||
test('da la vuelta en los dos extremos', () {
|
||||
// A dead button at the end of a list reads as a broken app on a car
|
||||
// screen, where there is no visible list position to explain it.
|
||||
expect(emisoraVecina(c, [a, b, c], haciaAtras: false), a);
|
||||
expect(emisoraVecina(a, [a, b, c], haciaAtras: true), c);
|
||||
});
|
||||
|
||||
test('identifica por uuid, no por instancia: un snapshot refrescado trae '
|
||||
'objetos distintos', () {
|
||||
final copiaDeB = Emisora(
|
||||
uuid: 'b',
|
||||
nombre: 'otro nombre',
|
||||
url: 'https://example.com/cambiada',
|
||||
);
|
||||
expect(emisoraVecina(copiaDeB, [a, b, c], haciaAtras: false), c);
|
||||
});
|
||||
|
||||
test('no hace nada sin contexto suficiente', () {
|
||||
expect(emisoraVecina(null, [a, b], haciaAtras: false), isNull);
|
||||
expect(emisoraVecina(a, [a], haciaAtras: false), isNull);
|
||||
expect(emisoraVecina(a, const [], haciaAtras: false), isNull);
|
||||
expect(
|
||||
emisoraVecina(emisora('fuera'), [a, b], haciaAtras: false),
|
||||
isNull,
|
||||
reason: 'una emisora que no está en la lista no debe saltar a ciegas',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('listaParaSaltoEmisora', () {
|
||||
test('favoritos gana: "siguiente" desde un favorito va al siguiente '
|
||||
'favorito, no a la entrada 4318 del catálogo', () {
|
||||
expect(
|
||||
listaParaSaltoEmisora(
|
||||
actual: a,
|
||||
favoritos: [a, b],
|
||||
misEmisoras: [a, c],
|
||||
todas: [a, b, c],
|
||||
),
|
||||
[a, b],
|
||||
);
|
||||
});
|
||||
|
||||
test('mis emisoras cuando no es favorita', () {
|
||||
expect(
|
||||
listaParaSaltoEmisora(
|
||||
actual: c,
|
||||
favoritos: [a, b],
|
||||
misEmisoras: [c, a],
|
||||
todas: [a, b, c],
|
||||
),
|
||||
[c, a],
|
||||
);
|
||||
});
|
||||
|
||||
test('cae al catálogo completo para una emisora llegada por búsqueda', () {
|
||||
expect(
|
||||
listaParaSaltoEmisora(
|
||||
actual: c,
|
||||
favoritos: [a],
|
||||
misEmisoras: [b],
|
||||
todas: [a, b, c],
|
||||
),
|
||||
[a, b, c],
|
||||
);
|
||||
});
|
||||
|
||||
test('vacía si no está en ninguna: el salto queda en no-op', () {
|
||||
expect(
|
||||
listaParaSaltoEmisora(
|
||||
actual: emisora('huerfana'),
|
||||
favoritos: [a],
|
||||
misEmisoras: [b],
|
||||
todas: [a, b],
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:audio_session/audio_session.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/servicio_audio_session.dart';
|
||||
|
||||
/// Reported from the car: PluriWave disappeared from the Android Auto pane
|
||||
/// mid-drive and another media app took its slot, the playback screen sat
|
||||
/// frozen, and the chosen equalizer was lost whenever navigation, a
|
||||
/// speed-camera app or the car's voice assistant spoke.
|
||||
///
|
||||
/// One cause behind all three. `androidWillPauseWhenDucked: true` made
|
||||
/// `audio_session` translate Android's AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
|
||||
/// into a full PAUSE. In a car that fires on every navigation instruction. A
|
||||
/// pause publishes `playing: false`, `AudioService.setState` turns that into
|
||||
/// `exitPlayingState()` and — with `androidStopForegroundOnPause` — into
|
||||
/// `stopForeground(...)`. The plugin's own doc for that flag: «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.
|
||||
///
|
||||
/// Ducking keeps `playing: true` throughout, so session, notification and
|
||||
/// car pane all survive the interruption.
|
||||
class _ObjetivoFalso implements ObjetivoAudioInterrumpible {
|
||||
bool atenuado = false;
|
||||
int pausas = 0;
|
||||
int reanudaciones = 0;
|
||||
int reaplicacionesEq = 0;
|
||||
|
||||
@override
|
||||
bool intencionReproducir = true;
|
||||
|
||||
@override
|
||||
bool estaReproduciendo = true;
|
||||
|
||||
@override
|
||||
Future<void> pausar() async => pausas++;
|
||||
|
||||
@override
|
||||
Future<void> reanudar() async => reanudaciones++;
|
||||
|
||||
@override
|
||||
Future<void> setAtenuado(bool valor) async => atenuado = valor;
|
||||
|
||||
@override
|
||||
Future<void> reaplicarEcualizador() async => reaplicacionesEq++;
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('un aviso de navegación ATENÚA, nunca pausa', () async {
|
||||
final objetivo = _ObjetivoFalso();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.duck),
|
||||
);
|
||||
|
||||
expect(objetivo.atenuado, isTrue);
|
||||
expect(
|
||||
objetivo.pausas,
|
||||
0,
|
||||
reason:
|
||||
'pausar publica playing:false -> exitPlayingState -> el servicio '
|
||||
'sale de primer plano y Android puede matarlo; ahí es donde la app '
|
||||
'desaparecía del panel del coche',
|
||||
);
|
||||
});
|
||||
|
||||
test('al terminar el aviso se recupera el volumen Y se reasienta el '
|
||||
'ecualizador', () async {
|
||||
final objetivo = _ObjetivoFalso();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.duck),
|
||||
);
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.duck),
|
||||
);
|
||||
|
||||
expect(objetivo.atenuado, isFalse);
|
||||
expect(
|
||||
objetivo.reaplicacionesEq,
|
||||
1,
|
||||
reason:
|
||||
'Android puede desactivar en silencio el efecto de esta app cuando '
|
||||
'otro cliente de mayor prioridad toma el foco, y el id de sesión no '
|
||||
'cambia, así que el disparador por rotación de sesión no salta',
|
||||
);
|
||||
expect(objetivo.pausas, 0);
|
||||
});
|
||||
|
||||
test('una llamada entrante SÍ pausa: no todo es un aviso corto', () async {
|
||||
// The duck change must not turn a real, non-duckable focus loss into a
|
||||
// station that keeps playing over a phone call.
|
||||
final objetivo = _ObjetivoFalso();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(true, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.pausas, 1);
|
||||
|
||||
await servicio.manejarInterrupcion(
|
||||
AudioInterruptionEvent(false, AudioInterruptionType.pause),
|
||||
);
|
||||
|
||||
expect(objetivo.reanudaciones, 1);
|
||||
expect(objetivo.reaplicacionesEq, 1);
|
||||
});
|
||||
|
||||
test('desconectar los auriculares pausa y NO reanuda solo', () async {
|
||||
final objetivo = _ObjetivoFalso();
|
||||
final servicio = ServicioAudioSession(objetivo: objetivo);
|
||||
|
||||
await servicio.manejarDesconexionSalida();
|
||||
|
||||
expect(objetivo.pausas, 1);
|
||||
expect(objetivo.reanudaciones, 0);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user