feat(alarmas): replace one-line reliability button with full diagnostics screen
Surface all six DiagnosticoAlarmasAndroid fields instead of three: the battery-optimization exemption and native pending-alarm count were already collected but silently dropped by the old widget. Each failing signal now offers a "Fix this" action that opens the right system settings screen (exact alarms, notifications, full-screen intent, battery optimization), guarded by SDK level and never crashing when a ROM lacks that screen. Manufacturers known for aggressive background killing (Xiaomi/Redmi/POCO, Huawei, Oppo, Vivo, OnePlus, Samsung) get an honest explanation that Autostart must be enabled manually, since there is no API to detect or grant it. Notifications now deep-links straight to ACTION_APP_NOTIFICATION_SETTINGS via a new openNotificationSettings native method, instead of reusing the runtime permission popup meant for first-time alarm creation. New copy is added to all 13 ARB locales with real per-language translations (not Spanish copies), verified by the ARB parity and anti-copy tests plus the corruption scanner.
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../estado/estado_alarmas.dart';
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import '../servicios/diagnostico_alarmas.dart';
|
||||
import '../servicios/servicio_alarmas_android.dart';
|
||||
import '../tema/pluriwave_theme.dart';
|
||||
import '../widgets/pluri_glass_surface.dart';
|
||||
import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
|
||||
/// Full Android alarm-reliability diagnostics screen (fix/alarmas-fiabilidad).
|
||||
///
|
||||
/// Replaces the old one-line `_AccesoDiagnostico` button in
|
||||
/// `pantalla_alarmas.dart`, which only ever surfaced 3 of the 6 fields
|
||||
/// `DiagnosticoAlarmasAndroid` collects. This screen shows all five
|
||||
/// diagnosable signals with a clear ok/needs-attention state, a "Fix this"
|
||||
/// action that opens the right system settings screen for each failing one,
|
||||
/// plus manufacturer-specific guidance for vendors known to require manually
|
||||
/// enabling Autostart.
|
||||
class PantallaDiagnosticoAlarmas extends StatelessWidget {
|
||||
const PantallaDiagnosticoAlarmas({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final estado = context.watch<EstadoAlarmas>();
|
||||
final diag = estado.diagnostico;
|
||||
|
||||
return PluriPushScaffold(
|
||||
title: l10n.androidReliabilityTitle,
|
||||
body:
|
||||
diag == null
|
||||
? ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: [
|
||||
PluriGlassSurface(
|
||||
child: Text(l10n.alarmDiagnosticsUnavailableHint),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _CuerpoDiagnostico(estado: estado, diag: diag),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CuerpoDiagnostico extends StatelessWidget {
|
||||
const _CuerpoDiagnostico({required this.estado, required this.diag});
|
||||
|
||||
final EstadoAlarmas estado;
|
||||
final DiagnosticoAlarmasAndroid diag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final items = construirItemsDiagnosticoAlarmas(
|
||||
diagnostico: diag,
|
||||
hayAlarmasActivas: estado.alarmas.any((alarma) => alarma.activa),
|
||||
);
|
||||
final mostrarAutostart = fabricanteRequiereGuiaAutostart(diag.fabricante);
|
||||
|
||||
return ListView(
|
||||
padding: PluriLayout.pageContentPadding,
|
||||
children: [
|
||||
for (final item in items) ...[
|
||||
_FilaDiagnostico(item: item, estado: estado, diag: diag),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
const SizedBox(height: 6),
|
||||
PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_FilaInformativa(
|
||||
titulo: l10n.alarmDiagnosticsManufacturerLabel,
|
||||
valor: diag.fabricante,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
_FilaInformativa(
|
||||
titulo: l10n.alarmDiagnosticsSdkLabel,
|
||||
valor: diag.versionSdk.toString(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (mostrarAutostart) ...[
|
||||
const SizedBox(height: 16),
|
||||
_GuiaAutostart(fabricante: diag.fabricante),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilaDiagnostico extends StatelessWidget {
|
||||
const _FilaDiagnostico({
|
||||
required this.item,
|
||||
required this.estado,
|
||||
required this.diag,
|
||||
});
|
||||
|
||||
final ItemDiagnosticoAlarma item;
|
||||
final EstadoAlarmas estado;
|
||||
final DiagnosticoAlarmasAndroid diag;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
final ok = item.estado == EstadoSenalDiagnostico.ok;
|
||||
final color = ok ? tokens.liveGreen : Theme.of(context).colorScheme.error;
|
||||
final esConteoNativo =
|
||||
item.senal == SenalDiagnosticoAlarma.alarmasNativasPendientes;
|
||||
|
||||
return PluriGlassSurface(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
ok ? Icons.check_circle_rounded : Icons.warning_amber_rounded,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_tituloSenal(l10n, item.senal),
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
ok
|
||||
? l10n.statusOk
|
||||
: l10n.alarmDiagnosticsNeedsAttentionStatus,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (esConteoNativo) ...[
|
||||
Text(
|
||||
l10n.alarmDiagnosticsNativeCountValue(
|
||||
diag.alarmasNativasPendientes,
|
||||
),
|
||||
),
|
||||
if (!ok) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(l10n.alarmDiagnosticsNativeCountAttentionHint),
|
||||
],
|
||||
] else
|
||||
Text(_hintSenal(l10n, item.senal)),
|
||||
if (!ok && item.accion != AccionDiagnosticoAlarma.ninguna) ...[
|
||||
const SizedBox(height: 8),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _ejecutarAccion(context, l10n),
|
||||
child: Text(l10n.alarmDiagnosticsFixAction),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs the system action for [item.accion] and reloads the diagnostic
|
||||
/// snapshot. Never throws across the widget boundary: every underlying
|
||||
/// `PuertoAlarmasAndroid` call already reports `false` instead (native side
|
||||
/// catches any intent-resolution failure), and a `false` here surfaces a
|
||||
/// calm SnackBar instead of leaving the tap looking like a no-op.
|
||||
Future<void> _ejecutarAccion(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
) async {
|
||||
final resuelto = switch (item.accion) {
|
||||
AccionDiagnosticoAlarma.abrirAlarmasExactas =>
|
||||
await estado.android.solicitarPermisoAlarmasExactas(),
|
||||
AccionDiagnosticoAlarma.abrirNotificaciones =>
|
||||
await estado.android.abrirConfiguracionNotificaciones(),
|
||||
AccionDiagnosticoAlarma.abrirOptimizacionBateria =>
|
||||
await estado.android.solicitarExencionBateria(),
|
||||
AccionDiagnosticoAlarma.abrirPantallaCompleta =>
|
||||
await estado.android.solicitarPermisoPantallaCompleta(),
|
||||
AccionDiagnosticoAlarma.ninguna => true,
|
||||
};
|
||||
await estado.cargarDiagnostico();
|
||||
if (!resuelto && context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.alarmDiagnosticsIntentUnavailable)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _tituloSenal(AppLocalizations l10n, SenalDiagnosticoAlarma senal) =>
|
||||
switch (senal) {
|
||||
SenalDiagnosticoAlarma.alarmasExactas =>
|
||||
l10n.alarmDiagnosticsExactAlarmsTitle,
|
||||
SenalDiagnosticoAlarma.notificaciones =>
|
||||
l10n.alarmDiagnosticsNotificationsTitle,
|
||||
SenalDiagnosticoAlarma.pantallaCompleta =>
|
||||
l10n.alarmDiagnosticsFullScreenTitle,
|
||||
SenalDiagnosticoAlarma.optimizacionBateria =>
|
||||
l10n.alarmDiagnosticsBatteryTitle,
|
||||
SenalDiagnosticoAlarma.alarmasNativasPendientes =>
|
||||
l10n.alarmDiagnosticsNativeCountTitle,
|
||||
};
|
||||
|
||||
/// Static one-line explanation per signal. `alarmasNativasPendientes` builds
|
||||
/// its own dynamic body in [_FilaDiagnostico] instead (count + conditional
|
||||
/// attention hint), so this branch is never actually rendered for it -- kept
|
||||
/// only so the switch stays exhaustive over the enum.
|
||||
String _hintSenal(
|
||||
AppLocalizations l10n,
|
||||
SenalDiagnosticoAlarma senal,
|
||||
) => switch (senal) {
|
||||
SenalDiagnosticoAlarma.alarmasExactas => l10n.alarmDiagnosticsExactAlarmsHint,
|
||||
SenalDiagnosticoAlarma.notificaciones =>
|
||||
l10n.alarmDiagnosticsNotificationsHint,
|
||||
SenalDiagnosticoAlarma.pantallaCompleta =>
|
||||
l10n.alarmDiagnosticsFullScreenHint,
|
||||
SenalDiagnosticoAlarma.optimizacionBateria =>
|
||||
l10n.alarmDiagnosticsBatteryHint,
|
||||
SenalDiagnosticoAlarma.alarmasNativasPendientes => '',
|
||||
};
|
||||
|
||||
class _FilaInformativa extends StatelessWidget {
|
||||
const _FilaInformativa({required this.titulo, required this.valor});
|
||||
|
||||
final String titulo;
|
||||
final String valor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(titulo),
|
||||
Text(valor, style: const TextStyle(fontWeight: FontWeight.w700)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Manufacturer-specific autostart explanation (fix/alarmas-fiabilidad item
|
||||
/// 3). Deliberately never claims the app can detect or grant this setting --
|
||||
/// there is no public API for it, so this is explanation only, never an
|
||||
/// action button.
|
||||
class _GuiaAutostart extends StatelessWidget {
|
||||
const _GuiaAutostart({required this.fabricante});
|
||||
|
||||
final String fabricante;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final tokens = context.pluriTokens;
|
||||
return PluriGlassSurface(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline_rounded, color: tokens.warmCoral),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.alarmDiagnosticsAutostartTitle,
|
||||
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(l10n.alarmDiagnosticsAutostartBody(fabricante)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user