mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 12:23:43 +00:00
fix(macos): request permission for download notifications (#1498)
This commit is contained in:
2 files changed
+153
-6
No files matched your search
@@ -28,13 +28,19 @@ class AppNotificationController extends AsyncNotifier<AppNotificationState> {
|
|||||||
StreamSubscription<TaskEvent>? _taskEventSubscription;
|
StreamSubscription<TaskEvent>? _taskEventSubscription;
|
||||||
var _notificationId = 0;
|
var _notificationId = 0;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
Stream<TaskEvent> get taskEvents => LibgopeedBoot.instance.taskEvents;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<AppNotificationState> build() async {
|
Future<AppNotificationState> build() async {
|
||||||
final runtime = ref.watch(appRuntimeControllerProvider).value;
|
final runtime = ref.watch(appRuntimeControllerProvider).value;
|
||||||
if (runtime == null || kIsWeb || !Util.isDesktop()) {
|
if (runtime == null || kIsWeb || !Util.isDesktop()) {
|
||||||
return const AppNotificationState();
|
return const AppNotificationState();
|
||||||
}
|
}
|
||||||
await _initNotifications(appLocalizationsFor(runtime.downloaderConfig.extra.locale));
|
await _initNotifications(
|
||||||
|
appLocalizationsFor(runtime.downloaderConfig.extra.locale),
|
||||||
|
requestPermissions: runtime.downloaderConfig.extra.desktopNotification,
|
||||||
|
);
|
||||||
_listenTaskEvents();
|
_listenTaskEvents();
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
unawaited(_taskEventSubscription?.cancel());
|
unawaited(_taskEventSubscription?.cancel());
|
||||||
@@ -42,11 +48,13 @@ class AppNotificationController extends AsyncNotifier<AppNotificationState> {
|
|||||||
return const AppNotificationState(started: true);
|
return const AppNotificationState(started: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initNotifications(AppLocalizations locale) async {
|
Future<void> _initNotifications(AppLocalizations locale, {required bool requestPermissions}) async {
|
||||||
const darwin = DarwinInitializationSettings(
|
// The runtime is watched, so enabling notifications in settings also
|
||||||
requestAlertPermission: false,
|
// requests authorization. macOS remembers previously granted/denied access.
|
||||||
|
final darwin = DarwinInitializationSettings(
|
||||||
|
requestAlertPermission: requestPermissions,
|
||||||
requestBadgePermission: false,
|
requestBadgePermission: false,
|
||||||
requestSoundPermission: false,
|
requestSoundPermission: requestPermissions,
|
||||||
);
|
);
|
||||||
final linux = LinuxInitializationSettings(
|
final linux = LinuxInitializationSettings(
|
||||||
defaultActionName: locale.open,
|
defaultActionName: locale.open,
|
||||||
@@ -92,7 +100,7 @@ class AppNotificationController extends AsyncNotifier<AppNotificationState> {
|
|||||||
|
|
||||||
void _listenTaskEvents() {
|
void _listenTaskEvents() {
|
||||||
_taskEventSubscription?.cancel();
|
_taskEventSubscription?.cancel();
|
||||||
_taskEventSubscription = LibgopeedBoot.instance.taskEvents.listen((event) async {
|
_taskEventSubscription = taskEvents.listen((event) async {
|
||||||
final runtime = ref.read(appRuntimeControllerProvider).value;
|
final runtime = ref.read(appRuntimeControllerProvider).value;
|
||||||
if (runtime?.downloaderConfig.extra.desktopNotification == false) return;
|
if (runtime?.downloaderConfig.extra.desktopNotification == false) return;
|
||||||
final locale = appLocalizationsFor(runtime?.downloaderConfig.extra.locale ?? '');
|
final locale = appLocalizationsFor(runtime?.downloaderConfig.extra.locale ?? '');
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gopeed/api/model/downloader_config.dart';
|
||||||
|
import 'package:gopeed/app/application/app_notification_controller.dart';
|
||||||
|
import 'package:gopeed/app/application/app_runtime_controller.dart';
|
||||||
|
import 'package:gopeed/core/common/api_server_state.dart';
|
||||||
|
import 'package:gopeed/core/common/start_config.dart';
|
||||||
|
import 'package:gopeed/core/common/task_event.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
const channel = MethodChannel('dexterous.com/flutter/local_notifications');
|
||||||
|
final calls = <MethodCall>[];
|
||||||
|
var permissionGranted = true;
|
||||||
|
late StreamController<TaskEvent> events;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||||
|
MacOSFlutterLocalNotificationsPlugin.registerWith();
|
||||||
|
calls.clear();
|
||||||
|
permissionGranted = true;
|
||||||
|
events = StreamController<TaskEvent>.broadcast();
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (call) async {
|
||||||
|
calls.add(call);
|
||||||
|
return permissionGranted;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
unawaited(events.close());
|
||||||
|
debugDefaultTargetPlatformOverride = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<ProviderContainer> start({required bool enabled}) async {
|
||||||
|
final container = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
appRuntimeControllerProvider.overrideWith(() => _TestRuntimeController(enabled)),
|
||||||
|
appNotificationControllerProvider.overrideWith(() => _TestNotificationController(events.stream)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
addTearDown(container.dispose);
|
||||||
|
await container.read(appRuntimeControllerProvider.future);
|
||||||
|
await container.read(appNotificationControllerProvider.future);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<dynamic, dynamic> initialization() => calls.lastWhere((call) => call.method == 'initialize').arguments as Map;
|
||||||
|
|
||||||
|
test('macOS requests alerts and sound when download notifications are enabled at startup', () async {
|
||||||
|
await start(enabled: true);
|
||||||
|
|
||||||
|
expect(initialization()['requestAlertPermission'], isTrue);
|
||||||
|
expect(initialization()['requestSoundPermission'], isTrue);
|
||||||
|
expect(initialization()['requestBadgePermission'], isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('disabled notifications defer authorization until the setting is enabled', () async {
|
||||||
|
final container = await start(enabled: false);
|
||||||
|
expect(initialization()['requestAlertPermission'], isFalse);
|
||||||
|
expect(initialization()['requestSoundPermission'], isFalse);
|
||||||
|
|
||||||
|
final runtime = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController;
|
||||||
|
runtime.setNotificationsEnabled(true);
|
||||||
|
await container.pump();
|
||||||
|
await container.read(appNotificationControllerProvider.future);
|
||||||
|
|
||||||
|
expect(initialization()['requestAlertPermission'], isTrue);
|
||||||
|
expect(initialization()['requestSoundPermission'], isTrue);
|
||||||
|
|
||||||
|
runtime.setNotificationsEnabled(false);
|
||||||
|
await container.pump();
|
||||||
|
await container.read(appNotificationControllerProvider.future);
|
||||||
|
expect(initialization()['requestAlertPermission'], isFalse);
|
||||||
|
expect(initialization()['requestSoundPermission'], isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('denied macOS authorization does not fail notification controller startup', () async {
|
||||||
|
permissionGranted = false;
|
||||||
|
final container = await start(enabled: true);
|
||||||
|
|
||||||
|
expect(container.read(appNotificationControllerProvider).requireValue.started, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('terminal task events notify once after enabling and stop after disabling', () async {
|
||||||
|
final container = await start(enabled: false);
|
||||||
|
final runtime = container.read(appRuntimeControllerProvider.notifier) as _TestRuntimeController;
|
||||||
|
const done = TaskEvent(type: TaskEventType.done, taskId: '1', name: 'archive.zip');
|
||||||
|
events.add(done);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(calls.where((call) => call.method == 'show'), isEmpty);
|
||||||
|
|
||||||
|
runtime.setNotificationsEnabled(true);
|
||||||
|
await container.pump();
|
||||||
|
await container.read(appNotificationControllerProvider.future);
|
||||||
|
events.add(done);
|
||||||
|
events.add(const TaskEvent(type: TaskEventType.error, taskId: '2', name: 'failed.zip'));
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
final notifications = calls.where((call) => call.method == 'show').toList();
|
||||||
|
expect(notifications, hasLength(2));
|
||||||
|
expect(notifications.map((call) => (call.arguments as Map)['body']), ['archive.zip', 'failed.zip']);
|
||||||
|
|
||||||
|
runtime.setNotificationsEnabled(false);
|
||||||
|
await container.pump();
|
||||||
|
await container.read(appNotificationControllerProvider.future);
|
||||||
|
events.add(done);
|
||||||
|
await Future<void>.delayed(Duration.zero);
|
||||||
|
expect(calls.where((call) => call.method == 'show'), hasLength(2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestNotificationController extends AppNotificationController {
|
||||||
|
_TestNotificationController(this.taskEvents);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final Stream<TaskEvent> taskEvents;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestRuntimeController extends AppRuntimeController {
|
||||||
|
_TestRuntimeController(this.enabled);
|
||||||
|
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<AppRuntimeState> build() async => _runtime(enabled);
|
||||||
|
|
||||||
|
void setNotificationsEnabled(bool enabled) => state = AsyncData(_runtime(enabled));
|
||||||
|
|
||||||
|
AppRuntimeState _runtime(bool enabled) => AppRuntimeState(
|
||||||
|
startConfig: StartConfig(),
|
||||||
|
apiServerState: ApiServerState.fromJson({}),
|
||||||
|
downloaderConfig: DownloaderConfig()..extra.desktopNotification = enabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user