feat(alarmas): add pure diagnostic mapping and autostart-guidance logic
DiagnosticoAlarmasAndroid already collected six raw reliability fields but only three ever reached the UI. Add a pure-Dart mapping that turns the raw snapshot into five ordered signals with a clear ok/needs- attention state (exact alarms, notifications, full-screen intent, battery-optimization exemption, native pending-alarm count), plus a manufacturer check for vendors known to require manually enabling Autostart (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung), since there is no public API to detect or grant that setting.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
import 'servicio_alarmas_android.dart';
|
||||
|
||||
/// One diagnosable Android alarm-reliability signal (fix/alarmas-fiabilidad).
|
||||
///
|
||||
/// [DiagnosticoAlarmasAndroid] already collects six raw fields, but only
|
||||
/// three were ever surfaced in the UI -- the two most diagnostic ones
|
||||
/// (battery-optimization exemption and the native pending-alarm count) were
|
||||
/// gathered and thrown away. This module maps the raw snapshot into a
|
||||
/// stable, ordered list of user-facing signals with a clear ok/needs-
|
||||
/// attention state, decoupled from Flutter/localization so it stays a
|
||||
/// trivial pure-Dart unit to test.
|
||||
enum SenalDiagnosticoAlarma {
|
||||
alarmasExactas,
|
||||
notificaciones,
|
||||
pantallaCompleta,
|
||||
optimizacionBateria,
|
||||
alarmasNativasPendientes,
|
||||
}
|
||||
|
||||
enum EstadoSenalDiagnostico { ok, atencion }
|
||||
|
||||
/// The system screen a "fix this" action should open for a given signal.
|
||||
/// `ninguna` marks signals with no actionable system screen of their own
|
||||
/// (`alarmasNativasPendientes` is informational -- fixing the OTHER signals
|
||||
/// above is what makes it recover).
|
||||
enum AccionDiagnosticoAlarma {
|
||||
abrirAlarmasExactas,
|
||||
abrirNotificaciones,
|
||||
abrirOptimizacionBateria,
|
||||
abrirPantallaCompleta,
|
||||
ninguna,
|
||||
}
|
||||
|
||||
class ItemDiagnosticoAlarma {
|
||||
const ItemDiagnosticoAlarma({
|
||||
required this.senal,
|
||||
required this.estado,
|
||||
required this.accion,
|
||||
});
|
||||
|
||||
final SenalDiagnosticoAlarma senal;
|
||||
final EstadoSenalDiagnostico estado;
|
||||
final AccionDiagnosticoAlarma accion;
|
||||
|
||||
bool get requiereAtencion => estado == EstadoSenalDiagnostico.atencion;
|
||||
}
|
||||
|
||||
/// Builds the five diagnosable signals in a FIXED, stable order so the
|
||||
/// screen renders them consistently every time.
|
||||
///
|
||||
/// [hayAlarmasActivas] contextualizes `alarmasNativasPendientes`: a fresh
|
||||
/// install with zero alarms turned on has nothing to register with the OS,
|
||||
/// so a `0` count there is only meaningful once the user actually has an
|
||||
/// active alarm -- THAT combination is direct evidence the alarm never
|
||||
/// reached the operating system at all, which is the single most useful
|
||||
/// signal for the reported "alarm never rings" failure mode.
|
||||
List<ItemDiagnosticoAlarma> construirItemsDiagnosticoAlarmas({
|
||||
required DiagnosticoAlarmasAndroid diagnostico,
|
||||
required bool hayAlarmasActivas,
|
||||
}) {
|
||||
EstadoSenalDiagnostico desde(bool ok) =>
|
||||
ok ? EstadoSenalDiagnostico.ok : EstadoSenalDiagnostico.atencion;
|
||||
|
||||
final alarmasNativasOk =
|
||||
!hayAlarmasActivas || diagnostico.alarmasNativasPendientes > 0;
|
||||
|
||||
return [
|
||||
ItemDiagnosticoAlarma(
|
||||
senal: SenalDiagnosticoAlarma.alarmasExactas,
|
||||
estado: desde(diagnostico.puedeProgramarExactas),
|
||||
accion: AccionDiagnosticoAlarma.abrirAlarmasExactas,
|
||||
),
|
||||
ItemDiagnosticoAlarma(
|
||||
senal: SenalDiagnosticoAlarma.notificaciones,
|
||||
estado: desde(diagnostico.notificacionesPermitidas),
|
||||
accion: AccionDiagnosticoAlarma.abrirNotificaciones,
|
||||
),
|
||||
ItemDiagnosticoAlarma(
|
||||
senal: SenalDiagnosticoAlarma.pantallaCompleta,
|
||||
estado: desde(diagnostico.puedeUsarPantallaCompleta),
|
||||
accion: AccionDiagnosticoAlarma.abrirPantallaCompleta,
|
||||
),
|
||||
ItemDiagnosticoAlarma(
|
||||
senal: SenalDiagnosticoAlarma.optimizacionBateria,
|
||||
estado: desde(diagnostico.ignoraOptimizacionBateria),
|
||||
accion: AccionDiagnosticoAlarma.abrirOptimizacionBateria,
|
||||
),
|
||||
ItemDiagnosticoAlarma(
|
||||
senal: SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
estado: desde(alarmasNativasOk),
|
||||
accion: AccionDiagnosticoAlarma.ninguna,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Manufacturers/sub-brands known for aggressive background-process killing
|
||||
/// that requires the user to manually enable "Autostart" (or the vendor's
|
||||
/// own equivalent toggle) -- there is NO public Android API to detect or
|
||||
/// grant this setting programmatically, so the app can only explain it.
|
||||
const _fabricantesConGuiaAutostart = [
|
||||
'xiaomi',
|
||||
'redmi',
|
||||
'poco',
|
||||
'huawei',
|
||||
'oppo',
|
||||
'vivo',
|
||||
'oneplus',
|
||||
'samsung',
|
||||
];
|
||||
|
||||
/// Whether [fabricante] (`Build.MANUFACTURER`, e.g. "Xiaomi", "POCO") is a
|
||||
/// known aggressive-background-killer vendor that needs the manual autostart
|
||||
/// explanation. Case-insensitive substring match, since `Build.MANUFACTURER`
|
||||
/// values are not fully standardized across sub-brands/regions/builds.
|
||||
bool fabricanteRequiereGuiaAutostart(String fabricante) {
|
||||
final normalizado = fabricante.trim().toLowerCase();
|
||||
if (normalizado.isEmpty) return false;
|
||||
return _fabricantesConGuiaAutostart.any(normalizado.contains);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:pluriwave/servicios/diagnostico_alarmas.dart';
|
||||
import 'package:pluriwave/servicios/servicio_alarmas_android.dart';
|
||||
|
||||
/// Builds a fully-OK snapshot by default; each test overrides only the
|
||||
/// field(s) it wants to fail, so failures are exercised independently.
|
||||
DiagnosticoAlarmasAndroid _diag({
|
||||
bool puedeProgramarExactas = true,
|
||||
bool notificacionesPermitidas = true,
|
||||
bool puedeUsarPantallaCompleta = true,
|
||||
bool ignoraOptimizacionBateria = true,
|
||||
int alarmasNativasPendientes = 1,
|
||||
String fabricante = 'Google',
|
||||
int versionSdk = 34,
|
||||
}) => DiagnosticoAlarmasAndroid(
|
||||
puedeProgramarExactas: puedeProgramarExactas,
|
||||
notificacionesPermitidas: notificacionesPermitidas,
|
||||
puedeUsarPantallaCompleta: puedeUsarPantallaCompleta,
|
||||
ignoraOptimizacionBateria: ignoraOptimizacionBateria,
|
||||
alarmasNativasPendientes: alarmasNativasPendientes,
|
||||
fabricante: fabricante,
|
||||
versionSdk: versionSdk,
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('construirItemsDiagnosticoAlarmas', () {
|
||||
test(
|
||||
'caso todo OK: los 5 items quedan en estado ok y en orden estable',
|
||||
() {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
expect(items, hasLength(5));
|
||||
expect(items.map((item) => item.senal).toList(), const [
|
||||
SenalDiagnosticoAlarma.alarmasExactas,
|
||||
SenalDiagnosticoAlarma.notificaciones,
|
||||
SenalDiagnosticoAlarma.pantallaCompleta,
|
||||
SenalDiagnosticoAlarma.optimizacionBateria,
|
||||
SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
]);
|
||||
expect(
|
||||
items.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
reason: 'ningun item deberia requerir atencion en el caso todo OK',
|
||||
);
|
||||
expect(items.any((item) => item.requiereAtencion), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('alarmas exactas queda en atencion cuando el permiso no esta '
|
||||
'concedido, sin afectar a los demas items (falla independiente)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(puedeProgramarExactas: false),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final exactas = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.alarmasExactas,
|
||||
);
|
||||
expect(exactas.estado, EstadoSenalDiagnostico.atencion);
|
||||
expect(exactas.accion, AccionDiagnosticoAlarma.abrirAlarmasExactas);
|
||||
expect(
|
||||
items
|
||||
.where(
|
||||
(item) => item.senal != SenalDiagnosticoAlarma.alarmasExactas,
|
||||
)
|
||||
.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('notificaciones queda en atencion cuando no estan permitidas, sin '
|
||||
'afectar a los demas items (falla independiente)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(notificacionesPermitidas: false),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final notificaciones = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.notificaciones,
|
||||
);
|
||||
expect(notificaciones.estado, EstadoSenalDiagnostico.atencion);
|
||||
expect(
|
||||
notificaciones.accion,
|
||||
AccionDiagnosticoAlarma.abrirNotificaciones,
|
||||
);
|
||||
expect(
|
||||
items
|
||||
.where(
|
||||
(item) => item.senal != SenalDiagnosticoAlarma.notificaciones,
|
||||
)
|
||||
.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('pantalla completa queda en atencion cuando no se puede usar, sin '
|
||||
'afectar a los demas items (falla independiente)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(puedeUsarPantallaCompleta: false),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final pantalla = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.pantallaCompleta,
|
||||
);
|
||||
expect(pantalla.estado, EstadoSenalDiagnostico.atencion);
|
||||
expect(pantalla.accion, AccionDiagnosticoAlarma.abrirPantallaCompleta);
|
||||
expect(
|
||||
items
|
||||
.where(
|
||||
(item) => item.senal != SenalDiagnosticoAlarma.pantallaCompleta,
|
||||
)
|
||||
.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('optimizacion de bateria queda en atencion cuando la app no esta '
|
||||
'exenta, sin afectar a los demas items (falla independiente)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(ignoraOptimizacionBateria: false),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final bateria = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.optimizacionBateria,
|
||||
);
|
||||
expect(bateria.estado, EstadoSenalDiagnostico.atencion);
|
||||
expect(bateria.accion, AccionDiagnosticoAlarma.abrirOptimizacionBateria);
|
||||
expect(
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
item.senal != SenalDiagnosticoAlarma.optimizacionBateria,
|
||||
)
|
||||
.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('alarmas nativas pendientes queda en atencion cuando hay alarmas '
|
||||
'activas pero ninguna llego a registrarse en el sistema (la senal '
|
||||
'mas diagnostica del reporte: el disparo nunca llego al SO)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(alarmasNativasPendientes: 0),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final nativas = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
);
|
||||
expect(nativas.estado, EstadoSenalDiagnostico.atencion);
|
||||
expect(nativas.accion, AccionDiagnosticoAlarma.ninguna);
|
||||
expect(
|
||||
items
|
||||
.where(
|
||||
(item) =>
|
||||
item.senal != SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
)
|
||||
.every((item) => item.estado == EstadoSenalDiagnostico.ok),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('alarmas nativas pendientes queda OK cuando no hay ninguna alarma '
|
||||
'activa, aunque el conteo nativo sea cero (nada deberia estar '
|
||||
'registrado todavia)', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(alarmasNativasPendientes: 0),
|
||||
hayAlarmasActivas: false,
|
||||
);
|
||||
|
||||
final nativas = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
);
|
||||
expect(nativas.estado, EstadoSenalDiagnostico.ok);
|
||||
});
|
||||
|
||||
test('alarmas nativas pendientes queda OK cuando hay alarmas activas y al '
|
||||
'menos una esta registrada en el sistema', () {
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: _diag(alarmasNativasPendientes: 3),
|
||||
hayAlarmasActivas: true,
|
||||
);
|
||||
|
||||
final nativas = items.singleWhere(
|
||||
(item) => item.senal == SenalDiagnosticoAlarma.alarmasNativasPendientes,
|
||||
);
|
||||
expect(nativas.estado, EstadoSenalDiagnostico.ok);
|
||||
});
|
||||
});
|
||||
|
||||
group('fabricanteRequiereGuiaAutostart', () {
|
||||
test('un fabricante desconocido no requiere guia de autostart', () {
|
||||
expect(fabricanteRequiereGuiaAutostart('Google'), isFalse);
|
||||
expect(fabricanteRequiereGuiaAutostart('Fairphone'), isFalse);
|
||||
expect(fabricanteRequiereGuiaAutostart(''), isFalse);
|
||||
});
|
||||
|
||||
test(
|
||||
'Xiaomi (y sus sub-marcas Redmi/POCO) requieren guia de autostart',
|
||||
() {
|
||||
expect(fabricanteRequiereGuiaAutostart('Xiaomi'), isTrue);
|
||||
expect(fabricanteRequiereGuiaAutostart('Redmi'), isTrue);
|
||||
expect(fabricanteRequiereGuiaAutostart('POCO'), isTrue);
|
||||
},
|
||||
);
|
||||
|
||||
test('otros fabricantes conocidos por matar procesos en segundo plano '
|
||||
'tambien requieren guia', () {
|
||||
for (final fabricante in [
|
||||
'HUAWEI',
|
||||
'OPPO',
|
||||
'vivo',
|
||||
'OnePlus',
|
||||
'samsung',
|
||||
]) {
|
||||
expect(
|
||||
fabricanteRequiereGuiaAutostart(fabricante),
|
||||
isTrue,
|
||||
reason: '$fabricante deberia requerir guia de autostart',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('la comparacion no distingue mayusculas/minusculas ni espacios', () {
|
||||
expect(fabricanteRequiereGuiaAutostart('XIAOMI'), isTrue);
|
||||
expect(fabricanteRequiereGuiaAutostart(' xiaomi '), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user