Files
TPSchedule/mobile/lib/features/profile/reminder_tools.dart
T

244 lines
8.5 KiB
Dart

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<ReminderTools> createState() => _ReminderToolsState();
}
class _ReminderToolsState extends State<ReminderTools> {
bool _busy = false;
Map<String, Object?>? _diagnosis;
String? _report;
void _notice(String text) =>
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(text)));
Future<void> _run(Future<void> 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<void> _test(bool scheduled) => _run(() async {
final message = await widget.controller.notifications.sendTestReminder(
scheduled: scheduled,
);
if (mounted) _notice(message);
});
Future<void> _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 = <String, Object?>{
'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',
'appNotificationsEnabled',
'channelExists',
'courseChannelEnabled',
'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<void> _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<String> _summary(Map<String, Object?> diagnosis) {
final rawSystem = diagnosis['system'];
final system = rawSystem is Map ? rawSystem : const {};
final pending = diagnosis['pendingCount'];
final sdk = system['sdk'];
final channelsSupported = sdk is! int || sdk >= 26;
return [
'通知权限:${_permission(diagnosis['notifications'])}',
if (diagnosis['appNotificationsEnabled'] is bool)
'系统通知总开关:${_permission(diagnosis['appNotificationsEnabled'])}',
if (channelsSupported && diagnosis['channelExists'] == true)
'课前提醒类别:${_permission(diagnosis['courseChannelEnabled'])}',
if (channelsSupported && diagnosis['channelExists'] == false)
'课前提醒类别尚未建立,请重新打开应用后检查。',
'精确提醒:${_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('复制提醒诊断'),
),
],
],
),
);
}