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:
@@ -18,6 +18,7 @@ import '../widgets/pluri_layout.dart';
|
||||
import '../widgets/pluri_push_scaffold.dart';
|
||||
import '../widgets/pluri_root_header.dart';
|
||||
import '../widgets/pluri_sleep_timer_sheet.dart';
|
||||
import 'pantalla_diagnostico_alarmas.dart';
|
||||
import 'pantalla_vacaciones.dart';
|
||||
|
||||
class PantallaAlarmas extends StatelessWidget {
|
||||
@@ -340,8 +341,7 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme
|
||||
.onSurface
|
||||
color: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
@@ -404,7 +404,9 @@ class _TarjetaAlarma extends StatelessWidget {
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
detalles.join(' · '),
|
||||
key: ValueKey('tarjeta-alarma-detalles-${alarma.id}'),
|
||||
key: ValueKey(
|
||||
'tarjeta-alarma-detalles-${alarma.id}',
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
@@ -1337,6 +1339,16 @@ class _SelectorEmisoraSheetState extends State<_SelectorEmisoraSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Entry point into the full Android alarm-reliability diagnostics screen
|
||||
/// (fix/alarmas-fiabilidad). Was a one-line `TextButton.icon` that only ever
|
||||
/// surfaced 3 of the 6 fields `DiagnosticoAlarmasAndroid` collects (exact
|
||||
/// alarms, notifications, full-screen intent) and cycled all three
|
||||
/// permission requests on a single tap; the two most diagnostic fields --
|
||||
/// battery-optimization exemption and the native pending-alarm count, which
|
||||
/// tells the user whether the alarm ever reached the OS at all -- were
|
||||
/// gathered and never shown. Now a tap target row (mirrors
|
||||
/// `_PanelVacaciones`'s shape) pushing `PantallaDiagnosticoAlarmas`, which
|
||||
/// shows every signal individually with its own fix action.
|
||||
class _AccesoDiagnostico extends StatelessWidget {
|
||||
const _AccesoDiagnostico({required this.estado});
|
||||
|
||||
@@ -1345,47 +1357,37 @@ class _AccesoDiagnostico extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final diag = estado.diagnostico;
|
||||
final exactStatus =
|
||||
diag?.puedeProgramarExactas == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
final notificationStatus =
|
||||
diag?.notificacionesPermitidas == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
final screenStatus =
|
||||
diag?.puedeUsarPantallaCompleta == true
|
||||
? l10n.statusOk
|
||||
: l10n.statusPending;
|
||||
return TextButton.icon(
|
||||
icon: const _AssetIcon(
|
||||
'assets/icons/alarmas/android_reliability.png',
|
||||
size: 28,
|
||||
),
|
||||
label: Text(
|
||||
diag == null
|
||||
? l10n.androidReliabilityTitle
|
||||
: l10n.androidReliabilityStatus(
|
||||
exactStatus,
|
||||
notificationStatus,
|
||||
screenStatus,
|
||||
final tokens = context.pluriTokens;
|
||||
return PluriGlassSurface(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
key: const ValueKey('diagnostico-alarmas-resumen'),
|
||||
borderRadius: BorderRadius.circular(tokens.radiusMd),
|
||||
onTap: () => _abrirDiagnostico(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
const _AssetIcon(
|
||||
'assets/icons/alarmas/android_reliability.png',
|
||||
size: 28,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(child: Text(l10n.androidReliabilityReview)),
|
||||
const Icon(Icons.chevron_right_rounded),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (diag != null && !diag.puedeProgramarExactas) {
|
||||
await estado.android.solicitarPermisoAlarmasExactas();
|
||||
}
|
||||
if (diag != null && !diag.notificacionesPermitidas) {
|
||||
await estado.android.solicitarPermisoNotificaciones();
|
||||
}
|
||||
if (diag != null && !diag.puedeUsarPantallaCompleta) {
|
||||
await estado.android.solicitarPermisoPantallaCompleta();
|
||||
}
|
||||
await estado.cargarDiagnostico();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _abrirDiagnostico(BuildContext context) {
|
||||
PluriPushScaffold.push(context, (_) => const PantallaDiagnosticoAlarmas());
|
||||
}
|
||||
}
|
||||
|
||||
/// Vacation summary row (alarm-vacation-ranges delta, WU8): replaces the old
|
||||
|
||||
@@ -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