import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../app/app_controller.dart'; import '../../app/app_theme.dart'; import '../../app/app_version.dart' as release; import '../../services/notification_service.dart'; class ReminderTools extends StatefulWidget { const ReminderTools({super.key, required this.controller}); final AppController controller; @override State createState() => _ReminderToolsState(); } class _ReminderToolsState extends State { bool _busy = false; Map? _diagnosis; String? _report; void _notice(String text) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text))); Future _run(Future Function() action) async { if (_busy) return; setState(() => _busy = true); try { await action(); } catch (error) { if (mounted) { _notice( error is ReminderSetupException ? error.message : '操作未完成,请检查系统设置后重试', ); } } finally { if (mounted) setState(() => _busy = false); } } Future _test(bool scheduled) => _run(() async { final message = await widget.controller.notifications.sendTestReminder( scheduled: scheduled, ); if (mounted) _notice(message); }); Future _check() => _run(() async { await widget.controller.refreshServices(); final diagnosis = await widget.controller.notifications.diagnose(); if (!mounted) return; final c = widget.controller; final s = c.snapshot.settings; final rawSystem = diagnosis['system']; final system = rawSystem is Map ? rawSystem : const {}; // Copy an explicit allowlist, never raw platform errors, courses or account. final report = { 'appVersion': '${release.appVersion}+${release.appBuildNumber}', 'checkedAt': c.clock().toIso8601String(), 'remindersEnabled': s.remindersEnabled, 'semesterStart': s.semester?.startDate.toIso8601String(), 'semesterWeeks': s.semester?.totalWeeks, 'reminderOffsets': s.reminderOffsets, 'courseCount': c.snapshot.courses.length, 'scheduledCount': c.reminderResult.scheduledCount, 'reminderStatus': c.reminderResult.status.name, for (final key in [ 'notifications', 'exactAlarm', 'pendingCount', 'channelImportance', 'channelSound', ]) key: _scalar(diagnosis[key]), 'activeQueryWarning': diagnosis['activeQueryWarning'] != null, 'errorCode': _errorCode(c.reminderErrorCode), 'systemError': _errorCode(diagnosis['systemError']), 'system': { for (final key in [ 'manufacturer', 'model', 'androidVersion', 'sdk', 'backgroundRestricted', 'batteryOptimizationsIgnored', 'powerSaveMode', 'interruptionFilter', ]) key: _scalar(system[key]), }, }; setState(() { _diagnosis = diagnosis; _report = const JsonEncoder.withIndent(' ').convert(report); }); }); Object? _scalar(Object? value) => value is String || value is bool || value is num ? value : null; String _errorCode(Object? value) { if (value is String && RegExp(r'^[a-zA-Z0-9_]{1,64}$').hasMatch(value)) { return value; } return value == null ? 'none' : 'unknown'; } Future _copy() => _run(() async { final report = _report; if (report == null) return; await Clipboard.setData(ClipboardData(text: report)); if (mounted) _notice('提醒诊断已复制,可粘贴到意见反馈中'); }); String _permission(Object? value) => switch (value) { true => '已允许', false => '未允许', _ => '暂时无法确认', }; List _summary(Map diagnosis) { final rawSystem = diagnosis['system']; final system = rawSystem is Map ? rawSystem : const {}; final pending = diagnosis['pendingCount']; return [ '通知权限:${_permission(diagnosis['notifications'])}', '精确提醒:${_permission(diagnosis['exactAlarm'])}', if (pending is int) '待发送提醒:$pending 条', if (diagnosis['notifications'] == false) '系统通知已关闭,请在通知设置中开启。', if (diagnosis['channelImportance'] == 0) '提醒通道已关闭,请在通知设置中开启。', if (diagnosis['channelSound'] == false) '提醒通道处于静音,请在通知设置中检查声音。', if (diagnosis['exactAlarm'] == false) '精确提醒未获允许,系统发送时间可能延迟。', if (system['backgroundRestricted'] == true || system['powerSaveMode'] == true) '系统正在限制后台运行,可在后台运行设置中检查。', if (system['interruptionFilter'] is int && system['interruptionFilter'] != 0 && system['interruptionFilter'] != 1) '勿扰模式可能影响提醒声音。', if (pending == 0 && widget.controller.snapshot.settings.remindersEnabled) '当前没有待发送提醒,请核对学期、课程日期和提醒提前时间。', if (diagnosis['activeQueryWarning'] != null) '部分系统提醒状态暂时无法读取,可稍后再检查。', if (diagnosis['systemError'] != null) '设备后台状态暂时无法读取,可在系统设置中检查。', ]; } @override Widget build(BuildContext context) => SurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '测试与检查', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: [ TextButton( onPressed: _busy ? null : () => _test(false), child: const Text('立即测试提醒'), ), TextButton( onPressed: _busy ? null : () => _test(true), child: const Text('10 秒后台测试'), ), ], ), const Text( '后台测试:点击后按 Home 键回到桌面,等待约 10 秒;请勿强行停止应用。', style: TextStyle(fontSize: 12, height: 1.6, color: river), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: [ TextButton( onPressed: _busy ? null : _check, child: const Text('检查提醒'), ), TextButton( onPressed: _busy ? null : () => _run( () => widget.controller.notifications.openSettings( background: false, ), ), child: const Text('通知设置'), ), TextButton( onPressed: _busy ? null : () => _run( () => widget.controller.notifications.openSettings( background: true, ), ), child: const Text('后台运行设置'), ), ], ), if (_busy) const Padding( padding: EdgeInsets.symmetric(vertical: 8), child: LinearProgressIndicator(), ), if (_diagnosis != null) ...[ const SizedBox(height: 8), for (final line in _summary(_diagnosis!)) Padding( padding: const EdgeInsets.only(bottom: 5), child: Text( line, style: const TextStyle(fontSize: 12, height: 1.5), ), ), TextButton( onPressed: _busy ? null : _copy, child: const Text('复制提醒诊断'), ), ], ], ), ); }