fix(alarmas,auto): guard the native snooze anchor, surface the EQ in the car
Three reported issues, two fixed and one instrumented. 1. Posponer left the alarm snoozed ~1444 minutes (24h04m). Traced end to end in Kotlin. onAlarmFired runs from the receiver BEFORE the ringing notification exists, and persists snoozeOriginMillis = null plus a triggerAtMillis already advanced to TOMORROW by computeNextTriggerMillis. snooze() then anchored on `spec.snoozeOriginMillis ?: spec.triggerAtMillis` and picked up tomorrow. The existing clamp could not catch it: it only rescues anchors in the PAST, so an anchor +24h out sails through. The countdown text is honest -- ceilMinutes(snoozeUntil - now) over Dart's own template -- the corrupt value is snoozeUntil. With N=5 and a tap at T+1min the arithmetic lands on 1444 exactly. This is the defecta9da855fixed on the Dart side with EstadoAlarmas._ocurrenciaSonando, after9c7cf4ehad fixed only one of two adjacent callers. The native lane never got that guard. Now it has anchorOccurrenceMillis, with a per-surface forward allowance: none for snooze() (the ringing notification closes an occurrence that has arrived) and a full PRE_NOTICE_MILLIS for postponeNext() (the pre-notice notification's occurrence legitimately has not). No Kotlin test source set exists in this project, so CI cannot verify this and no Dart test sees it (all use FakePuertoAlarmasAndroid). Verified by reading; needs an on-device pass. 2. The equalizer toggle stayed invisible on the Android Auto playback screen even on v1.2.14+136, which does contain it. On Android 13+ createCustomAction (AudioService.java:466-469) turns MediaControl.stop into a custom action too, so the car receives TWO in list order and stop was first -- a head unit exposing one custom-action slot shows that and buries the rest in an overflow menu. The equalizer now precedes stop and wins the slot; it is the better occupant, since the car has its own path to stop playback while the equalizer is reachable no other way from that screen. No platform detection needed, and the phone notification is untouched on every API level: nativeActions comes out [prev?, play/pause, stop, next?] below 13 and [prev?, play/pause, next?] on 13+, exactly as before. Both are now asserted. The list also moves to a public construirControlesTransporte. The guard test used to re-declare its own copy of the shape, so it stayed green while asserting a list lib/ no longer produced. It calls the real builder now. 3. Android Auto shows PLAY while a station is audibly playing: NOT fixed, deliberately. The car takes that icon from PlaybackStateCompat.getState() (AudioService.java:601-611), not from controls -- so none of the recent controls work can be the cause. All eight playbackState.add sites were audited and none publishes playing:false while audio runs, which leaves no traced input to fix. A proposed resync off bufferedPositionStream was rejected: it can publish a spurious idle, which AudioService.java:565-567 turns into stop() and tears down the foreground service -- the exact regressionabc6b47fixed, on the highest-frequency listener in the handler. Added instead a change-gated trace of the state actually published, with eqDisponible alongside it (that flag gates the equalizer action and is otherwise unobservable). One car session with `adb logcat -s ServicioAudio` settles both this and issue 2. Tests: 1124 -> 1127.
This commit is contained in:
@@ -336,19 +336,64 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The occurrence a "close this one" action is really acting on, never a
|
||||||
|
* future one.
|
||||||
|
*
|
||||||
|
* Reported on-device: pressing Posponer left the alarm snoozed for ~1444
|
||||||
|
* minutes (24h04m) instead of the configured few. The chain, all inside
|
||||||
|
* this file: [onAlarmFired] runs from the receiver BEFORE the ringing
|
||||||
|
* notification exists, and it persists `snoozeOriginMillis = null` plus a
|
||||||
|
* `triggerAtMillis` already advanced to TOMORROW by
|
||||||
|
* [computeNextTriggerMillis]. The snooze anchor was then plain
|
||||||
|
* `spec.snoozeOriginMillis ?: spec.triggerAtMillis`, so it picked up
|
||||||
|
* tomorrow. The old clamp (`if (target > now) target else now + minutes`)
|
||||||
|
* could not catch it: it only rescues anchors in the PAST, and an anchor
|
||||||
|
* +24h out sails straight through.
|
||||||
|
*
|
||||||
|
* [maxAheadMillis] is how far ahead an occurrence may legitimately sit for
|
||||||
|
* the calling surface: ~0 (just the shared imminence tolerance) for the
|
||||||
|
* ringing notification, but a full [PRE_NOTICE_MILLIS] for the pre-notice
|
||||||
|
* notification, whose occurrence has genuinely not happened yet.
|
||||||
|
*
|
||||||
|
* Mirrors `EstadoAlarmas._ocurrenciaSonando` on the Dart side, which was
|
||||||
|
* added in a9da855 for the exact same defect after 9c7cf4e had fixed only
|
||||||
|
* one of two adjacent callers. The native lane never got that guard.
|
||||||
|
* `lastHandledAtMillis` is the last fallback because [onAlarmFired] sets
|
||||||
|
* it to the occurrence that just rang -- note it is NOT purely native
|
||||||
|
* state (scheduleAlarm takes it from the Dart channel), so `now` has to
|
||||||
|
* remain the floor.
|
||||||
|
*/
|
||||||
|
private fun anchorOccurrenceMillis(
|
||||||
|
spec: NativeAlarmSpec,
|
||||||
|
now: Long,
|
||||||
|
maxAheadMillis: Long = 0L
|
||||||
|
): Long {
|
||||||
|
val limit = now + maxAheadMillis + IMMINENT_TOLERANCE_MILLIS
|
||||||
|
fun usable(candidate: Long?): Long? = candidate?.takeIf { it <= limit }
|
||||||
|
return usable(spec.snoozeOriginMillis)
|
||||||
|
?: usable(spec.triggerAtMillis)
|
||||||
|
?: usable(spec.lastHandledAtMillis)
|
||||||
|
?: now
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snoozes using the SAME anchor as [postponeNext] (Design 2.2): the
|
* Snoozes using the SAME anchor as [postponeNext] (Design 2.2): the
|
||||||
* occurrence time + minutes, clamped to now + minutes when the target is
|
* occurrence time + minutes, clamped to now + minutes when the target is
|
||||||
* already past. Returns the resulting snooze so the caller can report it
|
* already past. Returns the resulting snooze so the caller can report it
|
||||||
* back to Flutter (single source of truth), or null if the spec is gone.
|
* back to Flutter (single source of truth), or null if the spec is gone.
|
||||||
|
*
|
||||||
|
* The occurrence comes from [anchorOccurrenceMillis] with no forward
|
||||||
|
* allowance: this is the RINGING notification's button, so the occurrence
|
||||||
|
* it closes has already arrived.
|
||||||
*/
|
*/
|
||||||
fun snooze(id: String, minutes: Int): NativeSnoozeResult? {
|
fun snooze(id: String, minutes: Int): NativeSnoozeResult? {
|
||||||
cancelAutoSilence(id)
|
cancelAutoSilence(id)
|
||||||
val spec = readSpec(id) ?: return null
|
val spec = readSpec(id) ?: return null
|
||||||
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
||||||
val occurrenceAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
|
||||||
val target = occurrenceAt + safeMinutes * 60_000L
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
val occurrenceAt = anchorOccurrenceMillis(spec, now)
|
||||||
|
val target = occurrenceAt + safeMinutes * 60_000L
|
||||||
val snoozeUntil = if (target > now) target else now + safeMinutes * 60_000L
|
val snoozeUntil = if (target > now) target else now + safeMinutes * 60_000L
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
@@ -369,12 +414,24 @@ class AlarmScheduler(private val context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postpones from the PRE-NOTICE notification, whose occurrence has
|
||||||
|
* legitimately not arrived yet -- it is armed [PRE_NOTICE_MILLIS] ahead.
|
||||||
|
* So unlike [snooze] this allows an anchor that far forward, but no
|
||||||
|
* further: an anchor beyond that window is a spec already advanced to a
|
||||||
|
* later day, which is exactly the state that produced the reported ~24h
|
||||||
|
* snooze. See [anchorOccurrenceMillis].
|
||||||
|
*/
|
||||||
fun postponeNext(id: String, minutes: Int): Long? {
|
fun postponeNext(id: String, minutes: Int): Long? {
|
||||||
val spec = readSpec(id) ?: return null
|
val spec = readSpec(id) ?: return null
|
||||||
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
||||||
val occurrenceAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
|
||||||
val target = occurrenceAt + safeMinutes * 60_000L
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
val occurrenceAt = anchorOccurrenceMillis(
|
||||||
|
spec,
|
||||||
|
now,
|
||||||
|
maxAheadMillis = PRE_NOTICE_MILLIS
|
||||||
|
)
|
||||||
|
val target = occurrenceAt + safeMinutes * 60_000L
|
||||||
val snoozeUntil = if (target > now) target else now + safeMinutes * 60_000L
|
val snoozeUntil = if (target > now) target else now + safeMinutes * 60_000L
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
|
|||||||
@@ -328,6 +328,57 @@ List<MediaControl> controlesEcualizadorPersonalizados({
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The handler's full transport `controls` list for a `playbackState` push.
|
||||||
|
///
|
||||||
|
/// Top-level and public so tests exercise THIS function rather than a copy of
|
||||||
|
/// its shape. `servicio_audio_controles_notificacion_test.dart` used to
|
||||||
|
/// re-declare the list inline, which meant it stayed green while asserting a
|
||||||
|
/// shape `lib/` no longer produced — a guard that cannot see the thing it
|
||||||
|
/// guards. `PluriWaveAudioHandler` itself cannot be instantiated in a unit
|
||||||
|
/// test (a real `just_audio.AudioPlayer` needs platform MethodChannels), so
|
||||||
|
/// pulling the pure part out is the only way to test the real thing.
|
||||||
|
///
|
||||||
|
/// ORDER MATTERS, and only for the car.
|
||||||
|
///
|
||||||
|
/// On Android 13+ `createCustomAction` (AudioService.java:466-469) turns
|
||||||
|
/// [MediaControl.stop] into a `CUSTOM_ACTION_STOP` custom action too. So on a
|
||||||
|
/// modern phone the car receives TWO custom actions, in list order, and a head
|
||||||
|
/// unit that exposes a single custom-action slot shows only the first and
|
||||||
|
/// buries the rest in an overflow menu — which is why the equalizer toggle
|
||||||
|
/// stayed invisible on the playback screen even once it was back in this list
|
||||||
|
/// (reported on v1.2.14+136, which does contain it).
|
||||||
|
///
|
||||||
|
/// The equalizer therefore goes BEFORE `stop`, and wins that slot on purpose:
|
||||||
|
/// the car already has its own path to stop playback and Auto's template
|
||||||
|
/// renders play/pause itself, while the equalizer is reachable no other way
|
||||||
|
/// from this screen.
|
||||||
|
///
|
||||||
|
/// The phone notification is untouched by that ordering, on every API level.
|
||||||
|
/// `setState` (AudioService.java:513-521) splits this list by whether a
|
||||||
|
/// control carries a `customAction`: on 13+ `stop` goes to `customActions`
|
||||||
|
/// (never the notification) and the equalizer was never in `nativeActions`
|
||||||
|
/// anyway; below 13 the equalizer is the only custom action and `stop` stays
|
||||||
|
/// native. Either way `nativeActions` comes out as
|
||||||
|
/// `[prev?, play/pause, stop, next?]`, and `androidCompactActionIndices`
|
||||||
|
/// (`[colaActiva ? 1 : 0]`) still lands on play/pause.
|
||||||
|
List<MediaControl> construirControlesTransporte({
|
||||||
|
required bool colaActiva,
|
||||||
|
required bool playing,
|
||||||
|
required bool eqDisponible,
|
||||||
|
required bool eqActivo,
|
||||||
|
required AppLocalizations l10n,
|
||||||
|
}) => [
|
||||||
|
if (colaActiva) MediaControl.skipToPrevious,
|
||||||
|
if (playing) MediaControl.pause else MediaControl.play,
|
||||||
|
...controlesEcualizadorPersonalizados(
|
||||||
|
disponible: eqDisponible,
|
||||||
|
activo: eqActivo,
|
||||||
|
l10n: l10n,
|
||||||
|
),
|
||||||
|
MediaControl.stop,
|
||||||
|
if (colaActiva) MediaControl.skipToNext,
|
||||||
|
];
|
||||||
|
|
||||||
/// Content-style extras for the Ecualizador folder's items (decision
|
/// Content-style extras for the Ecualizador folder's items (decision
|
||||||
/// `auto/ecualizador-diseno`), mirrors `ConstructorArbolAuto
|
/// `auto/ecualizador-diseno`), mirrors `ConstructorArbolAuto
|
||||||
/// ._contentStyleLista` in `navegacion_auto.dart` — duplicated rather than
|
/// ._contentStyleLista` in `navegacion_auto.dart` — duplicated rather than
|
||||||
@@ -673,6 +724,7 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
speed: _player.speed,
|
speed: _player.speed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
_trazarEstadoPublicado();
|
||||||
});
|
});
|
||||||
|
|
||||||
_bufferedSub = _player.bufferedPositionStream.listen((pos) {
|
_bufferedSub = _player.bufferedPositionStream.listen((pos) {
|
||||||
@@ -704,49 +756,50 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The full transport `controls` list for a `playbackState` push (item 4):
|
String? _ultimaTrazaEstado;
|
||||||
/// the existing skip/play-pause/stop set, plus the equalizer's custom
|
|
||||||
/// actions appended at the end. Appending (rather than interleaving) keeps
|
/// Logs the state actually handed to `AudioService.setState`, once per real
|
||||||
/// [MediaControl.skipToPrevious]/play-pause/stop/[MediaControl.skipToNext]
|
/// change (this fires on every player event, so unconditional logging would
|
||||||
/// at their existing indices 0-3, so `androidCompactActionIndices`
|
/// bury the signal).
|
||||||
/// (`[colaActiva ? 1 : 0]`) stays correct unchanged.
|
|
||||||
///
|
///
|
||||||
/// A custom action here reaches the CAR ONLY, never the phone notification.
|
/// Exists for one open question that static reading could not settle: the
|
||||||
/// `AudioService.setState` (AudioService.java:513-520) splits the list in
|
/// Android Auto playback screen shows PLAY while a station is audibly
|
||||||
/// two: `createCustomAction` returns non-null for a control carrying a
|
/// playing. The car does NOT take that icon from `controls` — it takes it
|
||||||
/// `customAction`, and that control goes into `customActions` — which feeds
|
/// from `PlaybackStateCompat.getState()` (AudioService.java:601-611), where
|
||||||
/// `PlaybackStateCompat` and therefore the car's playback screen. Every
|
/// `ready` + `playing` is the only combination that yields `STATE_PLAYING`;
|
||||||
/// other control falls to the `else` branch and becomes a
|
/// `idle` gives `STATE_NONE`, which is what a freshly created session
|
||||||
/// `NotificationCompat.Action` in `nativeActions`, the list the media
|
/// carries (:319) and what a car would render as a play button. Every
|
||||||
/// notification is built from. The two never mix, so the equalizer toggle
|
/// `playbackState.add` in this file was audited and none publishes
|
||||||
/// cannot displace a transport button and cannot shift the indices
|
/// `playing: false` while audio runs, so the failing input is unknown and
|
||||||
/// `androidCompactActionIndices` points at.
|
/// any fix would be guesswork.
|
||||||
///
|
///
|
||||||
/// THE ONE RULE for anything added here with a `customAction`: its
|
/// `eqDisponible` rides along because the equalizer custom action is gated
|
||||||
/// `androidIcon` must name a drawable that really exists, and its `label`
|
/// on it and the flag is otherwise unobservable — one car session answers
|
||||||
/// must be non-empty in EVERY locale. `getResourceId` (:415-420) resolves
|
/// both questions at once:
|
||||||
/// the icon by name through `getIdentifier` and yields 0 when it misses,
|
/// adb logcat -s ServicioAudio
|
||||||
/// and `PlaybackStateCompat.CustomAction.Builder` throws on a 0 icon or an
|
void _trazarEstadoPublicado() {
|
||||||
/// empty label — a throw at :515 aborts `setState` before
|
final s = playbackState.value;
|
||||||
/// `mediaSession.setPlaybackState` (:552), taking the whole media session
|
final traza =
|
||||||
/// down with it. `servicio_audio_controles_notificacion_test.dart` holds
|
'proc=${s.processingState.name} playing=${s.playing} '
|
||||||
/// that line: it reads `android/app/src/main/res/drawable/` and fails if an
|
'eqDisponible=$_eqDisponible eqActivo=$_ecualizadorActivo '
|
||||||
/// icon named here has no file behind it.
|
'custom=${s.controls.where((c) => c.customAction != null).length} '
|
||||||
|
'controles=${s.controls.length}';
|
||||||
|
if (traza == _ultimaTrazaEstado) return;
|
||||||
|
_ultimaTrazaEstado = traza;
|
||||||
|
developer.log(traza, name: 'ServicioAudio');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Binds [construirControlesTransporte] — which holds the whole contract,
|
||||||
|
/// including why the equalizer must precede `stop` — to this handler's live
|
||||||
|
/// equalizer state.
|
||||||
List<MediaControl> _controlesTransporte({
|
List<MediaControl> _controlesTransporte({
|
||||||
required bool colaActiva,
|
required bool colaActiva,
|
||||||
required bool playing,
|
required bool playing,
|
||||||
}) => [
|
}) => construirControlesTransporte(
|
||||||
if (colaActiva) MediaControl.skipToPrevious,
|
colaActiva: colaActiva,
|
||||||
if (playing) MediaControl.pause else MediaControl.play,
|
playing: playing,
|
||||||
MediaControl.stop,
|
eqDisponible: _eqDisponible,
|
||||||
if (colaActiva) MediaControl.skipToNext,
|
eqActivo: _ecualizadorActivo,
|
||||||
..._controlesEqPersonalizados(),
|
|
||||||
];
|
|
||||||
|
|
||||||
List<MediaControl> _controlesEqPersonalizados() =>
|
|
||||||
controlesEcualizadorPersonalizados(
|
|
||||||
disponible: _eqDisponible,
|
|
||||||
activo: _ecualizadorActivo,
|
|
||||||
l10n: _textos,
|
l10n: _textos,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -137,55 +137,114 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('transport row keeps its shape', () {
|
group('transport row keeps its shape', () {
|
||||||
/// Mirrors `_controlesTransporte`'s construction. Kept in the test rather
|
// Calls the REAL builder, never a copy of it. This group used to
|
||||||
/// than reaching into the private member so the assertion documents the
|
// re-declare the list inline, which meant it stayed green while asserting
|
||||||
/// intended shape independently of the implementation.
|
// a shape lib/ no longer produced — a guard blind to the thing it guards.
|
||||||
List<MediaControl> transporte({
|
List<MediaControl> transporte({
|
||||||
required bool colaActiva,
|
required bool colaActiva,
|
||||||
required bool playing,
|
required bool playing,
|
||||||
required bool eqDisponible,
|
required bool eqDisponible,
|
||||||
}) => [
|
}) => construirControlesTransporte(
|
||||||
if (colaActiva) MediaControl.skipToPrevious,
|
colaActiva: colaActiva,
|
||||||
if (playing) MediaControl.pause else MediaControl.play,
|
playing: playing,
|
||||||
MediaControl.stop,
|
eqDisponible: eqDisponible,
|
||||||
if (colaActiva) MediaControl.skipToNext,
|
eqActivo: true,
|
||||||
...controlesEcualizadorPersonalizados(
|
|
||||||
disponible: eqDisponible,
|
|
||||||
activo: true,
|
|
||||||
l10n: lookupAppLocalizations(const Locale('es')),
|
l10n: lookupAppLocalizations(const Locale('es')),
|
||||||
),
|
);
|
||||||
];
|
|
||||||
|
/// What `AudioService.setState` (AudioService.java:513-521) would route to
|
||||||
|
/// `nativeActions` — the ONLY list the phone's media notification is built
|
||||||
|
/// from, and the list `androidCompactActionIndices` indexes into.
|
||||||
|
///
|
||||||
|
/// On Android 13+ `MediaControl.stop` also becomes a custom action
|
||||||
|
/// (:466-469), so pass [sdk33] to model that split.
|
||||||
|
List<MediaControl> nativas(
|
||||||
|
List<MediaControl> controles, {
|
||||||
|
required bool sdk33,
|
||||||
|
}) =>
|
||||||
|
controles
|
||||||
|
.where((c) => c.customAction == null)
|
||||||
|
.where((c) => !(sdk33 && c == MediaControl.stop))
|
||||||
|
.toList();
|
||||||
|
|
||||||
for (final colaActiva in [false, true]) {
|
for (final colaActiva in [false, true]) {
|
||||||
for (final playing in [false, true]) {
|
for (final playing in [false, true]) {
|
||||||
test('compact index still points at play/pause '
|
test('compact index still points at play/pause on BOTH API levels '
|
||||||
'(colaActiva=$colaActiva playing=$playing)', () {
|
'(colaActiva=$colaActiva playing=$playing)', () {
|
||||||
final controles = transporte(
|
final controles = transporte(
|
||||||
colaActiva: colaActiva,
|
colaActiva: colaActiva,
|
||||||
playing: playing,
|
playing: playing,
|
||||||
eqDisponible: true,
|
eqDisponible: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// androidCompactActionIndices is `[colaActiva ? 1 : 0]`. The EQ
|
|
||||||
// action is APPENDED, so the native transport buttons keep
|
|
||||||
// indices 0-3 and the collapsed shade still shows play/pause.
|
|
||||||
final indiceCompacto = colaActiva ? 1 : 0;
|
final indiceCompacto = colaActiva ? 1 : 0;
|
||||||
expect(controles.length, greaterThan(indiceCompacto));
|
|
||||||
|
for (final sdk33 in [false, true]) {
|
||||||
|
final row = nativas(controles, sdk33: sdk33);
|
||||||
|
expect(row.length, greaterThan(indiceCompacto));
|
||||||
expect(
|
expect(
|
||||||
controles[indiceCompacto],
|
row[indiceCompacto],
|
||||||
playing ? MediaControl.pause : MediaControl.play,
|
playing ? MediaControl.pause : MediaControl.play,
|
||||||
|
reason:
|
||||||
|
'androidCompactActionIndices is [colaActiva ? 1 : 0] and it '
|
||||||
|
'indexes nativeActions (AudioService.java:613-618, :637-639)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('the equalizer comes BEFORE stop, so it wins the first custom-action '
|
||||||
|
'slot on the car', () {
|
||||||
|
// Reported on v1.2.14+136: the toggle was in the binary but invisible on
|
||||||
|
// the head unit. On Android 13+ stop is ALSO a custom action, and it used
|
||||||
|
// to be first — a unit exposing one slot showed stop and buried the
|
||||||
|
// equalizer in an overflow menu.
|
||||||
|
final controles = transporte(
|
||||||
|
colaActiva: true,
|
||||||
|
playing: true,
|
||||||
|
eqDisponible: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// The custom action must never sit among the transport buttons:
|
final custom = controles.where(
|
||||||
// `nativeActions` and `customActions` are built by walking this
|
(c) => c.customAction != null || c == MediaControl.stop,
|
||||||
// list in order, so an interleaved custom action would renumber
|
);
|
||||||
// the notification's own actions.
|
expect(
|
||||||
final indiceCustom = controles.indexWhere(
|
custom.first.customAction,
|
||||||
(c) => c.customAction != null,
|
isNotNull,
|
||||||
|
reason: 'on Android 13+ this is the order the car receives them in',
|
||||||
);
|
);
|
||||||
expect(indiceCustom, controles.length - 1);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
test('reordering did NOT disturb the notification row', () {
|
||||||
|
// The whole safety argument for the swap: nativeActions must come out
|
||||||
|
// [prev?, play/pause, stop, next?] on <13 and [prev?, play/pause, next?]
|
||||||
|
// on 13+, exactly as before the equalizer moved.
|
||||||
|
final controles = transporte(
|
||||||
|
colaActiva: true,
|
||||||
|
playing: true,
|
||||||
|
eqDisponible: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(nativas(controles, sdk33: false), [
|
||||||
|
MediaControl.skipToPrevious,
|
||||||
|
MediaControl.pause,
|
||||||
|
MediaControl.stop,
|
||||||
|
MediaControl.skipToNext,
|
||||||
|
]);
|
||||||
|
expect(nativas(controles, sdk33: true), [
|
||||||
|
MediaControl.skipToPrevious,
|
||||||
|
MediaControl.pause,
|
||||||
|
MediaControl.skipToNext,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a device with no equalizer gets the exact pre-existing list', () {
|
||||||
|
expect(transporte(colaActiva: true, playing: true, eqDisponible: false), [
|
||||||
|
MediaControl.skipToPrevious,
|
||||||
|
MediaControl.pause,
|
||||||
|
MediaControl.stop,
|
||||||
|
MediaControl.skipToNext,
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user