fix: initialize reminder channel and clarify notification permission diagnostics
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
const appVersion = '0.7.8';
|
||||
const appBuildNumber = 27;
|
||||
const appBuildNumber = 28;
|
||||
const appFilingNumber = '蜀ICP备2025169276号-5A';
|
||||
|
||||
@@ -69,6 +69,9 @@ class _ReminderToolsState extends State<ReminderTools> {
|
||||
'reminderStatus': c.reminderResult.status.name,
|
||||
for (final key in [
|
||||
'notifications',
|
||||
'appNotificationsEnabled',
|
||||
'channelExists',
|
||||
'courseChannelEnabled',
|
||||
'exactAlarm',
|
||||
'pendingCount',
|
||||
'channelImportance',
|
||||
@@ -125,8 +128,16 @@ class _ReminderToolsState extends State<ReminderTools> {
|
||||
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) '系统通知已关闭,请在通知设置中开启。',
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
|
||||
import '../domain/course.dart';
|
||||
import '../domain/app_settings.dart';
|
||||
import '../domain/course_occurrence.dart';
|
||||
import 'diagnostic_log.dart';
|
||||
import 'notification_service.dart';
|
||||
|
||||
class ReminderPermissions {
|
||||
@@ -72,7 +73,8 @@ class LocalNotificationService extends NotificationService {
|
||||
await cancelOwned();
|
||||
return const ReminderResult(
|
||||
ReminderStatus.permissionDenied,
|
||||
message: '请允许通知,并检查系统中的“课前提醒”通知类别。',
|
||||
message:
|
||||
'通知未获允许,当前没有安排课前提醒。请在“通知设置”开启TPSchedule的通知总开关和“课前提醒”类别,再点“检查提醒”。',
|
||||
);
|
||||
}
|
||||
final upcoming =
|
||||
@@ -144,7 +146,7 @@ class LocalNotificationService extends NotificationService {
|
||||
if (!saved.containsAll(desired.keys)) {
|
||||
throw const ReminderSetupException(
|
||||
'schedule_not_saved',
|
||||
'系统未保存全部提醒,请点击“检查提醒”后重试。',
|
||||
'部分提醒尚未完成登记,请点击“检查提醒”后重试。',
|
||||
);
|
||||
}
|
||||
final message = [
|
||||
@@ -191,6 +193,12 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
final void Function(String key)? onTap;
|
||||
final _plugin = FlutterLocalNotificationsPlugin();
|
||||
static const _system = MethodChannel('com.tpschedule.app/reminders');
|
||||
static const _courseChannel = AndroidNotificationChannel(
|
||||
'hepan_lessons',
|
||||
'课前提醒',
|
||||
description: '课程开始前的提醒',
|
||||
importance: Importance.high,
|
||||
);
|
||||
String? _activeQueryWarning;
|
||||
Future<void>? _initializing;
|
||||
Future<void> initialize() => _initializing ??= Future<void>(() async {
|
||||
@@ -205,6 +213,9 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
if (ready != true) {
|
||||
throw StateError('通知服务初始化失败');
|
||||
}
|
||||
// Make the category visible before checking access or scheduling.
|
||||
// Reusing its ID preserves the user's existing Android channel choices.
|
||||
await _android?.createNotificationChannel(_courseChannel);
|
||||
final launch = await _plugin.getNotificationAppLaunchDetails();
|
||||
if (launch?.didNotificationLaunchApp == true) {
|
||||
_select(launch?.notificationResponse?.payload);
|
||||
@@ -239,9 +250,20 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
channel.id == 'hepan_lessons' &&
|
||||
channel.importance == Importance.none,
|
||||
);
|
||||
final appEnabled = await _android?.areNotificationsEnabled() ?? false;
|
||||
DiagnosticLog.instance.add(
|
||||
'reminder_access_checked',
|
||||
fields: {
|
||||
'result': appEnabled && courseChannelEnabled,
|
||||
'code': !appEnabled
|
||||
? 'app_notifications_disabled'
|
||||
: !courseChannelEnabled
|
||||
? 'course_channel_disabled'
|
||||
: 'allowed',
|
||||
},
|
||||
);
|
||||
return ReminderPermissions(
|
||||
(await _android?.areNotificationsEnabled() ?? false) &&
|
||||
courseChannelEnabled,
|
||||
appEnabled && courseChannelEnabled,
|
||||
await _android?.canScheduleExactNotifications() ?? false,
|
||||
);
|
||||
}
|
||||
@@ -305,7 +327,7 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
);
|
||||
final saved = await _plugin.pendingNotificationRequests();
|
||||
if (!saved.any((item) => item.id == -1002)) {
|
||||
throw const ReminderSetupException('test_not_saved', '系统未保存测试提醒。');
|
||||
throw const ReminderSetupException('test_not_saved', '测试提醒登记未完成。');
|
||||
}
|
||||
return '已安排测试,请按手机 Home 键回到桌面,10 秒后查看通知栏。';
|
||||
}
|
||||
@@ -327,6 +349,7 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
final channel = channels
|
||||
?.where((item) => item.id == 'hepan_lessons')
|
||||
.firstOrNull;
|
||||
final appEnabled = await _android?.areNotificationsEnabled();
|
||||
Map<Object?, Object?>? system;
|
||||
String? systemError;
|
||||
try {
|
||||
@@ -338,6 +361,11 @@ class PluginReminderGateway extends ReminderGateway {
|
||||
}
|
||||
return {
|
||||
'notifications': access.notifications,
|
||||
'appNotificationsEnabled': appEnabled,
|
||||
'channelExists': channels == null ? null : channel != null,
|
||||
'courseChannelEnabled': channel == null
|
||||
? null
|
||||
: channel.importance != Importance.none,
|
||||
'exactAlarm': access.exactAlarm,
|
||||
'pendingCount': pending.length,
|
||||
'channelImportance': channel?.importance.value,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: hepan_everyday
|
||||
description: TPSchedule 学生课表
|
||||
publish_to: none
|
||||
version: 0.7.8+27
|
||||
version: 0.7.8+28
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
dependencies:
|
||||
|
||||
@@ -7,6 +7,54 @@ import 'package:hepan_everyday/services/local_notification_service.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test('fresh notification initialization creates the course category before checks', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
AndroidFlutterLocalNotificationsPlugin.registerWith();
|
||||
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||
const system = MethodChannel('com.tpschedule.app/reminders');
|
||||
final messenger =
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
|
||||
final categories = <Map<Object?, Object?>>[];
|
||||
messenger.setMockMethodCallHandler(channel, (call) async {
|
||||
switch (call.method) {
|
||||
case 'initialize':
|
||||
return true;
|
||||
case 'createNotificationChannel':
|
||||
final data = call.arguments as Map;
|
||||
categories.add({
|
||||
...Map<Object?, Object?>.from(data),
|
||||
// Android returns a numeric color even when no LED was set.
|
||||
'ledColor': data['ledColor'] ?? 0,
|
||||
});
|
||||
return null;
|
||||
case 'getNotificationAppLaunchDetails':
|
||||
return {'notificationLaunchedApp': false};
|
||||
case 'areNotificationsEnabled':
|
||||
case 'canScheduleExactNotifications':
|
||||
return true;
|
||||
case 'getNotificationChannels':
|
||||
return categories;
|
||||
case 'pendingNotificationRequests':
|
||||
return [];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
messenger.setMockMethodCallHandler(
|
||||
system,
|
||||
(_) async => <Object?, Object?>{},
|
||||
);
|
||||
addTearDown(() {
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
messenger.setMockMethodCallHandler(channel, null);
|
||||
messenger.setMockMethodCallHandler(system, null);
|
||||
});
|
||||
final gateway = PluginReminderGateway();
|
||||
final diagnosis = await gateway.diagnose();
|
||||
expect(diagnosis['channelImportance'], Importance.high.value);
|
||||
expect(diagnosis['appNotificationsEnabled'], isTrue);
|
||||
expect(diagnosis['channelExists'], isTrue);
|
||||
expect(categories.single['id'], 'hepan_lessons');
|
||||
});
|
||||
test(
|
||||
'querying future reminders does not depend on active notification access',
|
||||
() async {
|
||||
|
||||
@@ -70,6 +70,42 @@ Future<void> _open(WidgetTester tester, _Notifications notifications) async {
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'Android 7 does not ask the user to create an unsupported category',
|
||||
(tester) async {
|
||||
final notifications = _Notifications()
|
||||
..diagnosis = Completer<Map<String, Object?>>();
|
||||
notifications.diagnosis!.complete({
|
||||
'notifications': true,
|
||||
'appNotificationsEnabled': true,
|
||||
'channelExists': false,
|
||||
'system': {'sdk': 24},
|
||||
});
|
||||
await _open(tester, notifications);
|
||||
await tester.tap(find.text('检查提醒'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.textContaining('类别尚未建立'), findsNothing);
|
||||
expect(find.text('系统通知总开关:已允许'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'diagnosis separates the application switch from the reminder category',
|
||||
(tester) async {
|
||||
final notifications = _Notifications()
|
||||
..diagnosis = Completer<Map<String, Object?>>();
|
||||
notifications.diagnosis!.complete({
|
||||
'notifications': false,
|
||||
'appNotificationsEnabled': false,
|
||||
'channelExists': true,
|
||||
'courseChannelEnabled': true,
|
||||
});
|
||||
await _open(tester, notifications);
|
||||
await tester.tap(find.text('检查提醒'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('系统通知总开关:未允许'), findsOneWidget);
|
||||
expect(find.text('课前提醒类别:已允许'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
testWidgets('test controls distinguish immediate and background reminders', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
Reference in New Issue
Block a user