feat(alarm): make the ring immune to device media volume
The alarm's steady-state audio runs on the Flutter media-stream player after the native handoff, so device volume 0 silenced it entirely. The ring now forces STREAM_MUSIC to an audible reference: Dart requests the override before pre-starting alarm audio (fallback WAV included), Kotlin captures the current volume once and restores it idempotently on every exit path (dismiss, snooze, dispose), with a native best-effort backstop in service teardown. The backstop is handoff-aware via PluriWaveAlarmService.flutterOwnsRing: confirmFlutterAudio marks the handoff before triggering the native stop, so the backstop cannot restore the volume mid-ring right as the Flutter player takes over (that would re-silence the alarm at volume 0). The flag resets at every ring start; Flutter process death after handoff remains a documented best-effort gap. The alarm's perceived loudness keeps ramping 5% to the configured volume through the player as before; normal radio playback and call ducking never touch the override. Work unit 2/3 of alarm-volume-ramp-restore (ring volume override).
This commit is contained in:
@@ -17,6 +17,14 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
bool ignoraOptimizacionBateria = true;
|
||||
int solicitudesExencionBateria = 0;
|
||||
|
||||
/// Records each [forzarVolumenMediaParaAlarma] call (Slice 2: ring-scoped
|
||||
/// device-volume override).
|
||||
final volumenForzado = <double>[];
|
||||
|
||||
/// Counts [restaurarVolumenMedia] calls (Slice 2). A plain counter, not a
|
||||
/// list: widget tests only need to assert how many times restore ran.
|
||||
int volumenRestaurado = 0;
|
||||
|
||||
/// Test-only failure switch (Design D7): when true, [programar] throws
|
||||
/// instead of scheduling, enabling failure-path coverage that the fake
|
||||
/// could not otherwise produce.
|
||||
@@ -59,6 +67,16 @@ class FakePuertoAlarmasAndroid implements PuertoAlarmasAndroid {
|
||||
detenidas.add(alarmaId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> forzarVolumenMediaParaAlarma(double fraccion) async {
|
||||
volumenForzado.add(fraccion);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> restaurarVolumenMedia() async {
|
||||
volumenRestaurado++;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<DiagnosticoAlarmasAndroid> diagnostico() async =>
|
||||
DiagnosticoAlarmasAndroid(
|
||||
|
||||
@@ -356,4 +356,91 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('PantallaAlarmaSonando media-volume override restore (Slice 2)', () {
|
||||
testWidgets('detener: restaura el volumen de medios exactamente una vez', (
|
||||
tester,
|
||||
) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
|
||||
await _montarConHistorial(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(env.android.volumenRestaurado, 1);
|
||||
});
|
||||
|
||||
testWidgets('posponer: restaura el volumen de medios exactamente una vez', (
|
||||
tester,
|
||||
) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
|
||||
await _montarConHistorial(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.alarmSnoozeOptionLabel(5)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(env.android.volumenRestaurado, 1);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'detener: _silenciarAudio y dispose en secuencia no duplican la '
|
||||
'restauracion (idempotencia en el call-site Dart)',
|
||||
(tester) async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
|
||||
await _montarConHistorial(
|
||||
tester,
|
||||
android: env.android,
|
||||
estadoAlarmas: env.estadoAlarmas,
|
||||
radio: env.radio,
|
||||
);
|
||||
|
||||
await tester.tap(find.text(l10n.stopAlarmAction));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Proves dispose() really ran too (both call sites fired) — the
|
||||
// idempotence guard must still cap the counter at 1.
|
||||
expect(find.byType(PantallaAlarmaSonando), findsNothing);
|
||||
expect(
|
||||
env.android.volumenRestaurado,
|
||||
1,
|
||||
reason:
|
||||
'restaurarVolumenMedia debe invocarse a lo sumo una vez por '
|
||||
'pantalla, aunque _silenciarAudio (dentro de _detener) y '
|
||||
'dispose() ambos lo llamen',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('EstadoRadio reproduccion normal nunca dispara el override de volumen '
|
||||
'(Slice 2, guardia de regresion)', () {
|
||||
test('ciclo de reproducir/pausar fuera de una alarma no toca el canal '
|
||||
'de volumen de medios', () async {
|
||||
final env = await _buildEnv();
|
||||
addTearDown(env.dispose);
|
||||
final emisora = env.estadoAlarmas.alarmas.single.emisora!;
|
||||
|
||||
await env.radio.reproducir(emisora);
|
||||
await env.radio.audio.pausar();
|
||||
|
||||
expect(env.android.volumenForzado, isEmpty);
|
||||
expect(env.android.volumenRestaurado, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -106,4 +106,32 @@ void main() {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'forzarVolumenMediaParaAlarma invoca overrideMediaVolumeForRing con fraction',
|
||||
() async {
|
||||
final servicio = ServicioAlarmasAndroid(channel: channel);
|
||||
|
||||
await servicio.forzarVolumenMediaParaAlarma(1.0);
|
||||
|
||||
final llamada = llamadas.singleWhere(
|
||||
(c) => c.method == 'overrideMediaVolumeForRing',
|
||||
);
|
||||
expect(llamada.arguments, {'fraction': 1.0});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'restaurarVolumenMedia invoca restoreMediaVolume sin argumentos',
|
||||
() async {
|
||||
final servicio = ServicioAlarmasAndroid(channel: channel);
|
||||
|
||||
await servicio.restaurarVolumenMedia();
|
||||
|
||||
final llamada = llamadas.singleWhere(
|
||||
(c) => c.method == 'restoreMediaVolume',
|
||||
);
|
||||
expect(llamada.arguments, <String, Object?>{});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user