Compare commits
3
Commits
62f7804d6d
...
ec6ccb2db8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec6ccb2db8 | ||
|
|
8e00dc0c7c | ||
|
|
f01c0911f7 |
@@ -134,6 +134,36 @@ jobs:
|
|||||||
- name: Build APK release
|
- name: Build APK release
|
||||||
run: flutter build apk --release
|
run: flutter build apk --release
|
||||||
|
|
||||||
|
# Guardian de recursos: el APK debe contener los drawables que el codigo
|
||||||
|
# resuelve POR NOMBRE en runtime (getResources().getIdentifier).
|
||||||
|
#
|
||||||
|
# Un nombre que no resuelve devuelve id 0, y eso no falla la
|
||||||
|
# compilacion: falla en el coche. Concretamente
|
||||||
|
# PlaybackStateCompat.CustomAction.Builder lanza con icono 0, ese throw
|
||||||
|
# aborta AudioService.setState antes de activar la sesion de medios, y
|
||||||
|
# Android Auto se queda con la interfaz congelada. Paso exactamente eso
|
||||||
|
# entre el 31-07 (commit 2540556) y el 07-08 sin que nada lo detectara.
|
||||||
|
#
|
||||||
|
# Anadir un drawable nuevo referenciado por nombre => anadirlo aqui.
|
||||||
|
- name: Verificar recursos criticos en el APK
|
||||||
|
run: |
|
||||||
|
APK=build/app/outputs/flutter-apk/app-release.apk
|
||||||
|
FALTAN=0
|
||||||
|
for RECURSO in ic_auto_eq_on ic_auto_eq_off ic_stat_pluriwave; do
|
||||||
|
if unzip -l "$APK" | grep -q "res/drawable/$RECURSO.xml"; then
|
||||||
|
echo "OK res/drawable/$RECURSO.xml"
|
||||||
|
else
|
||||||
|
echo "FALTA res/drawable/$RECURSO.xml"
|
||||||
|
FALTAN=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$FALTAN" -ne 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Hay drawables resueltos por nombre que NO estan en el APK."
|
||||||
|
echo "En runtime resolveran a id 0 y tumbaran la sesion de medios."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build AAB release
|
- name: Build AAB release
|
||||||
run: flutter build appbundle --release
|
run: flutter build appbundle --release
|
||||||
|
|
||||||
|
|||||||
@@ -936,15 +936,26 @@ Future<void> reproducirPorMediaId(
|
|||||||
await reproducir(item);
|
await reproducir(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which list previous/next should walk for [actual]: the NARROWEST list the
|
/// Which list previous/next should walk for [actual]: the NARROWEST context
|
||||||
/// station actually belongs to, favourites first, then my stations, then the
|
/// the station belongs to.
|
||||||
/// full catalogue.
|
|
||||||
///
|
///
|
||||||
/// Narrowest-first is the point. "Next station" while playing a favourite
|
/// Tightest first:
|
||||||
/// should land on the next favourite, not on entry 4,318 of a 50,000-station
|
/// 1. its FAVOURITES GROUP, when it is a favourite filed under a real group,
|
||||||
/// catalogue that happens to sit beside it alphabetically. Falling through to
|
/// 2. all favourites,
|
||||||
/// [todas] only when the station is in neither curated list keeps the button
|
/// 3. my stations,
|
||||||
/// working for a station reached by search.
|
/// 4. the full catalogue.
|
||||||
|
///
|
||||||
|
/// The group tier is what the owner asked for: driving with a themed group,
|
||||||
|
/// "next" should stay inside that group rather than wander across every
|
||||||
|
/// favourite. And "next" from a favourite must never land 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 alive for a station reached by search.
|
||||||
|
///
|
||||||
|
/// [GrupoFavoritos.sinAsignarId] is deliberately NOT treated as a group: it
|
||||||
|
/// is the ABSENCE of one, so those stations walk all favourites instead of a
|
||||||
|
/// bucket that only means "unfiled". A group with a single member also falls
|
||||||
|
/// through to all favourites — otherwise both buttons would be dead ends.
|
||||||
///
|
///
|
||||||
/// Returns an empty list when [actual] is in none of them, which
|
/// Returns an empty list when [actual] is in none of them, which
|
||||||
/// [emisoraVecina] turns into "do nothing".
|
/// [emisoraVecina] turns into "do nothing".
|
||||||
@@ -954,10 +965,28 @@ List<Emisora> listaParaSaltoEmisora({
|
|||||||
required List<Emisora> misEmisoras,
|
required List<Emisora> misEmisoras,
|
||||||
required List<Emisora> todas,
|
required List<Emisora> todas,
|
||||||
}) {
|
}) {
|
||||||
bool contiene(List<Emisora> lista) => lista.any((e) => e.uuid == actual.uuid);
|
Emisora? enLista(List<Emisora> lista) {
|
||||||
if (contiene(favoritos)) return favoritos;
|
for (final e in lista) {
|
||||||
if (contiene(misEmisoras)) return misEmisoras;
|
if (e.uuid == actual.uuid) return e;
|
||||||
if (contiene(todas)) return todas;
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The FAVOURITE record is the authority on the group, never `actual`: the
|
||||||
|
// playing station is rebuilt from a MediaItem by `emisoraDesdeMediaItem`,
|
||||||
|
// which carries no group id and would always report "sin asignar".
|
||||||
|
final favorita = enLista(favoritos);
|
||||||
|
if (favorita != null) {
|
||||||
|
final grupo = favorita.grupoFavoritosId;
|
||||||
|
if (grupo != GrupoFavoritos.sinAsignarId) {
|
||||||
|
final delGrupo =
|
||||||
|
favoritos.where((e) => e.grupoFavoritosId == grupo).toList();
|
||||||
|
if (delGrupo.length > 1) return delGrupo;
|
||||||
|
}
|
||||||
|
return favoritos;
|
||||||
|
}
|
||||||
|
if (enLista(misEmisoras) != null) return misEmisoras;
|
||||||
|
if (enLista(todas) != null) return todas;
|
||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1544,6 +1544,15 @@ class PluriWaveAudioHandler extends BaseAudioHandler
|
|||||||
todas: await fuente.todas(),
|
todas: await fuente.todas(),
|
||||||
);
|
);
|
||||||
final destino = emisoraVecina(actual, lista, haciaAtras: haciaAtras);
|
final destino = emisoraVecina(actual, lista, haciaAtras: haciaAtras);
|
||||||
|
// Reported: in the car these buttons did nothing for radio. Every early
|
||||||
|
// return here is silent, so the log has to say WHICH one fired --
|
||||||
|
// an empty list (the station matched none of the three) and a station
|
||||||
|
// that is in a list of one are indistinguishable from outside.
|
||||||
|
debugPrint(
|
||||||
|
'[PluriWave][ServicioAudio] salto emisora atras=$haciaAtras '
|
||||||
|
'actual=${actual.nombre} uuid=${actual.uuid} '
|
||||||
|
'lista=${lista.length} destino=${destino?.nombre ?? "NINGUNO"}',
|
||||||
|
);
|
||||||
if (destino == null) return;
|
if (destino == null) return;
|
||||||
await playMediaItem(mediaItemParaEmisora(destino, l10n: _textos));
|
await playMediaItem(mediaItemParaEmisora(destino, l10n: _textos));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:pluriwave/modelos/emisora.dart';
|
import 'package:pluriwave/modelos/emisora.dart';
|
||||||
|
import 'package:pluriwave/modelos/grupo_favoritos.dart';
|
||||||
import 'package:pluriwave/servicios/navegacion_auto.dart';
|
import 'package:pluriwave/servicios/navegacion_auto.dart';
|
||||||
|
|
||||||
/// Requested: the Android Auto playback screen must offer previous/next for
|
/// Requested: the Android Auto playback screen must offer previous/next for
|
||||||
@@ -99,4 +100,76 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('navegación por GRUPO de favoritos (pedido por el dueño)', () {
|
||||||
|
Emisora favorita(String uuid, String grupo) => Emisora(
|
||||||
|
uuid: uuid,
|
||||||
|
nombre: uuid,
|
||||||
|
url: 'https://example.com/$uuid',
|
||||||
|
grupoFavoritosId: grupo,
|
||||||
|
);
|
||||||
|
|
||||||
|
final rock1 = favorita('rock1', 'g-rock');
|
||||||
|
final rock2 = favorita('rock2', 'g-rock');
|
||||||
|
final jazz1 = favorita('jazz1', 'g-jazz');
|
||||||
|
final suelta = favorita('suelta', GrupoFavoritos.sinAsignarId);
|
||||||
|
|
||||||
|
test('sonando una favorita de un grupo, se recorre SOLO ese grupo', () {
|
||||||
|
expect(
|
||||||
|
listaParaSaltoEmisora(
|
||||||
|
actual: rock1,
|
||||||
|
favoritos: [rock1, jazz1, rock2, suelta],
|
||||||
|
misEmisoras: const [],
|
||||||
|
todas: [rock1, jazz1, rock2, suelta],
|
||||||
|
),
|
||||||
|
[rock1, rock2],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('el grupo se lee del registro de FAVORITOS, no de lo que suena: la '
|
||||||
|
'emisora reconstruida desde el MediaItem no lleva grupo', () {
|
||||||
|
// emisoraDesdeMediaItem no puede saber el grupo -> llega "sin asignar".
|
||||||
|
final reconstruida = Emisora(
|
||||||
|
uuid: 'rock1',
|
||||||
|
nombre: 'Rock 1',
|
||||||
|
url: 'https://example.com/rock1',
|
||||||
|
);
|
||||||
|
expect(reconstruida.grupoFavoritosId, GrupoFavoritos.sinAsignarId);
|
||||||
|
expect(
|
||||||
|
listaParaSaltoEmisora(
|
||||||
|
actual: reconstruida,
|
||||||
|
favoritos: [rock1, jazz1, rock2],
|
||||||
|
misEmisoras: const [],
|
||||||
|
todas: const [],
|
||||||
|
),
|
||||||
|
[rock1, rock2],
|
||||||
|
reason: 'si se leyera de `actual` caeríamos a todos los favoritos',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"sin asignar" NO es un grupo: recorre todos los favoritos', () {
|
||||||
|
expect(
|
||||||
|
listaParaSaltoEmisora(
|
||||||
|
actual: suelta,
|
||||||
|
favoritos: [rock1, suelta, jazz1],
|
||||||
|
misEmisoras: const [],
|
||||||
|
todas: const [],
|
||||||
|
),
|
||||||
|
[rock1, suelta, jazz1],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('un grupo de UNA sola emisora cae a todos los favoritos, para no '
|
||||||
|
'dejar los dos botones muertos', () {
|
||||||
|
expect(
|
||||||
|
listaParaSaltoEmisora(
|
||||||
|
actual: jazz1,
|
||||||
|
favoritos: [rock1, rock2, jazz1],
|
||||||
|
misEmisoras: const [],
|
||||||
|
todas: const [],
|
||||||
|
),
|
||||||
|
[rock1, rock2, jazz1],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user