feat(macos): add macOS permissions service for Input Monitoring, Local Network, and Notifications (#21)
* feat(macos): add macOS permissions service for Input Monitoring, Local Network, and Notifications Add MacOSPermissionsService (Dart) + native MethodChannel handler (Swift) for macOS-specific permissions not covered by permission_handler: - Input Monitoring (CGPreflightListenEventAccess / CGRequestListenEventAccess) for global PTT via Event Tap - Local Network Privacy prompt (NWBrowser for _ts3._tcp, macOS 15+) - Notifications (UNUserNotificationCenter authorization) Trace: SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091 Changes: - Info.plist: add NSBonjourServices array with _ts3._tcp - macos_permissions_service.dart: Dart service with MethodChannel, ValueNotifier states, PTT capability derivation (L0Focused / L1MacOSEventTap), non-macOS short-circuit - MainFlutterWindow.swift: native handler registered as FlutterPlugin, Input Monitoring check/request/polling, NWBrowser trigger with denial detection, UNUserNotificationCenter request - main.dart: wire service into bootstrap lifecycle, listen for PTT capability changes from Input Monitoring state - macos_permissions_service_test.dart: 17 unit tests covering inbound state changes, outbound calls, lifecycle, error handling, platform behavior (179/179 full suite pass) * fix(macos): keep permissions capability state live
This commit is contained in:
@@ -23,6 +23,7 @@ import 'services/audio_lifecycle_service.dart';
|
||||
import 'services/channel_join_error_mapper.dart';
|
||||
import 'services/connection_phase_state.dart';
|
||||
import 'services/ios_permissions_service.dart';
|
||||
import 'services/macos_permissions_service.dart';
|
||||
import 'services/prefetch_debouncer.dart';
|
||||
import 'services/snapshot_state_mapper.dart';
|
||||
import 'services/ts3_server_link.dart';
|
||||
@@ -378,6 +379,12 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
final AndroidPermissionsService _androidPermissions =
|
||||
AndroidPermissionsService();
|
||||
final IosPermissionsService _iosPermissions = IosPermissionsService();
|
||||
// SRS-198 / SRS-297 / SRS-300: macOS Input Monitoring, Local Network,
|
||||
// and Notifications permission service. On non-macOS hosts the service
|
||||
// short-circuits to "granted" / "unsupported" and never wires the
|
||||
// MethodChannel.
|
||||
final MacOSPermissionsService _macOSPermissions =
|
||||
MacOSPermissionsService();
|
||||
final UiPreferencesService _uiPreferences = const UiPreferencesService();
|
||||
|
||||
@override
|
||||
@@ -399,6 +406,16 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_iosPermissions.recordAudioState.addListener(
|
||||
_onRecordAudioPermissionChanged,
|
||||
);
|
||||
// SRS-198 / SRS-297 / SRS-300: start macOS permission service.
|
||||
// On non-macOS this is a no-op. On macOS, it checks Input
|
||||
// Monitoring state and begins polling for changes so the PTT
|
||||
// capability badge upgrades from L0Focused → L1MacOSEventTap
|
||||
// when the user grants the permission in System Settings.
|
||||
_macOSPermissions.start();
|
||||
_macOSPermissions.pttCapabilityState.addListener(
|
||||
_onMacOSPttCapabilityChanged,
|
||||
);
|
||||
_macOSPermissions.checkInitialStates();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_requestRecordAudioOnStartup());
|
||||
});
|
||||
@@ -498,6 +515,28 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/// SRS-198 / SRS-297 / SRS-300 / SDD-091: React to macOS Input
|
||||
/// Monitoring state changes by updating the PTT capability level.
|
||||
/// When the macOS permissions service detects that Input Monitoring
|
||||
/// has been granted (via polling), it emits `L1MacOSEventTap` which
|
||||
/// overrides the bridge-emitted `L0Focused` default.
|
||||
void _onMacOSPttCapabilityChanged() {
|
||||
final level = _macOSPermissions.pttCapabilityState.value;
|
||||
// Only override if the macOS service has a resolved state
|
||||
// different from the current bridge-emitted level, and only
|
||||
// on macOS.
|
||||
if (_isMacOS && level != _pttLevel) {
|
||||
setState(() {
|
||||
_pttLevel = level;
|
||||
if (level == 'L1MacOSEventTap') {
|
||||
_pttBackendId = 'macos-event-tap';
|
||||
} else if (level == 'L0Focused') {
|
||||
_pttBackendId = 'focused';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearPermissionHardMute() async {
|
||||
if (!_hardMuteByPermission || _permissionHardMuteClearInFlight) return;
|
||||
_permissionHardMuteClearInFlight = true;
|
||||
@@ -936,11 +975,16 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
_iosPermissions.recordAudioState.removeListener(
|
||||
_onRecordAudioPermissionChanged,
|
||||
);
|
||||
// SRS-198 / SRS-297: detach macOS permission listeners.
|
||||
_macOSPermissions.pttCapabilityState.removeListener(
|
||||
_onMacOSPttCapabilityChanged,
|
||||
);
|
||||
// SDD-106: detach the Kotlin -> Dart MethodChannel handler so a
|
||||
// late invokeMethod from the platform side cannot land on this
|
||||
// disposed state.
|
||||
_androidPermissions.stop();
|
||||
_iosPermissions.stop();
|
||||
_macOSPermissions.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
/// macOS permission integration for Input Monitoring, Local Network,
|
||||
/// and Notifications.
|
||||
///
|
||||
/// Trace:
|
||||
/// - SRS-198 (Push-to-talk system permission acquisition).
|
||||
/// - SRS-297 / SRS-300 (Input Monitoring for global PTT on macOS).
|
||||
/// - SysRS-166 (Desktop notifications).
|
||||
/// - SDD-091 (PTT capability badge — live capability level).
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// * Subscribe to the Swift-side `MethodChannel`
|
||||
/// `app.chanora/macos_permissions` for inbound state-change
|
||||
/// invocations emitted by the native handler in
|
||||
/// `MainFlutterWindow.swift` (Swift → Dart).
|
||||
/// * Provide imperative Dart → Swift entry points for checking and
|
||||
/// requesting Input Monitoring, triggering the Local Network
|
||||
/// prompt, and requesting notification authorization.
|
||||
/// * Expose the latest resolved states as [ValueListenable] so UI
|
||||
/// surfaces (PTT capability badge, permission banners, connection
|
||||
/// error messages) can react without polling.
|
||||
///
|
||||
/// ## Non-macOS short-circuit
|
||||
///
|
||||
/// On Android / iOS / Linux / Windows / web, none of these macOS-
|
||||
/// specific permissions exist. The MethodChannel is therefore never
|
||||
/// constructed off-macOS. Each state listenable stays at its default
|
||||
/// "granted" / "not needed" value so all consumers become no-ops.
|
||||
///
|
||||
/// ## No global statics
|
||||
///
|
||||
/// Following the pattern established by `AndroidPermissionsService`
|
||||
/// (SDD-106) and `BackIntentService` (SDD-028), this class is
|
||||
/// constructor-injected. The host app instantiates one instance at
|
||||
/// startup and passes it through the widget tree.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Channel constants
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// MethodChannel name shared with Swift `MacOSPermissionsHandler`.
|
||||
@visibleForTesting
|
||||
const String macOSPermissionsChannelName =
|
||||
'app.chanora/macos_permissions';
|
||||
|
||||
// Outbound (Dart → Swift) method names.
|
||||
@visibleForTesting
|
||||
const String methodCheckInputMonitoring = 'checkInputMonitoring';
|
||||
@visibleForTesting
|
||||
const String methodRequestInputMonitoring = 'requestInputMonitoring';
|
||||
@visibleForTesting
|
||||
const String methodOpenInputMonitoringSettings =
|
||||
'openInputMonitoringSettings';
|
||||
@visibleForTesting
|
||||
const String methodTriggerLocalNetworkPrompt = 'triggerLocalNetworkPrompt';
|
||||
@visibleForTesting
|
||||
const String methodCheckLocalNetwork = 'checkLocalNetwork';
|
||||
@visibleForTesting
|
||||
const String methodRequestNotifications = 'requestNotifications';
|
||||
@visibleForTesting
|
||||
const String methodCheckNotifications = 'checkNotifications';
|
||||
|
||||
// Inbound (Swift → Dart) method names.
|
||||
@visibleForTesting
|
||||
const String methodInputMonitoringStateChanged =
|
||||
'inputMonitoringStateChanged';
|
||||
@visibleForTesting
|
||||
const String methodLocalNetworkStateChanged = 'localNetworkStateChanged';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Enums
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Discrete states for macOS-specific permissions.
|
||||
enum MacOSPermissionState {
|
||||
/// Permission granted.
|
||||
granted,
|
||||
|
||||
/// Permission denied by the user.
|
||||
denied,
|
||||
|
||||
/// Permission has not yet been determined (first launch before
|
||||
/// any prompt, or the system returned an unexpected value).
|
||||
notDetermined,
|
||||
|
||||
/// No resolved state yet (cold launch before the first emission,
|
||||
/// or non-macOS host before short-circuit). Consumers treat this
|
||||
/// as "not yet known".
|
||||
unknown,
|
||||
}
|
||||
|
||||
/// Local Network permission states, extended to cover macOS 14 and
|
||||
/// earlier where Local Network Privacy does not exist.
|
||||
enum MacOSLocalNetworkState {
|
||||
/// Permission granted or the Local Network prompt was satisfied.
|
||||
granted,
|
||||
|
||||
/// Permission explicitly denied by the user (macOS 15+ only).
|
||||
denied,
|
||||
|
||||
/// No prompt shown yet.
|
||||
notDetermined,
|
||||
|
||||
/// Running on macOS 14 or earlier where Local Network Privacy
|
||||
/// does not apply. Consumers treat this as "granted".
|
||||
unsupported,
|
||||
|
||||
/// No resolved state yet.
|
||||
unknown,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State parsing helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
MacOSPermissionState _parsePermissionState(String? raw) {
|
||||
switch (raw) {
|
||||
case 'Granted':
|
||||
return MacOSPermissionState.granted;
|
||||
case 'Denied':
|
||||
return MacOSPermissionState.denied;
|
||||
case 'NotDetermined':
|
||||
return MacOSPermissionState.notDetermined;
|
||||
default:
|
||||
return MacOSPermissionState.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
MacOSLocalNetworkState _parseLocalNetworkState(String? raw) {
|
||||
switch (raw) {
|
||||
case 'Granted':
|
||||
return MacOSLocalNetworkState.granted;
|
||||
case 'Denied':
|
||||
return MacOSLocalNetworkState.denied;
|
||||
case 'NotDetermined':
|
||||
return MacOSLocalNetworkState.notDetermined;
|
||||
case 'Unsupported':
|
||||
return MacOSLocalNetworkState.unsupported;
|
||||
default:
|
||||
return MacOSLocalNetworkState.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PTT capability level mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Maps the Input Monitoring state to the PTT capability level string
|
||||
/// consumed by [PttCapabilityBadge].
|
||||
///
|
||||
/// Trace: SDD-091 (capability badge); desktop-ptt-architecture.md
|
||||
/// (macOS Event Tap strategy).
|
||||
String _pttCapabilityLevel(MacOSPermissionState inputMonitoring) {
|
||||
switch (inputMonitoring) {
|
||||
case MacOSPermissionState.granted:
|
||||
return 'L1MacOSEventTap';
|
||||
case MacOSPermissionState.denied:
|
||||
case MacOSPermissionState.notDetermined:
|
||||
case MacOSPermissionState.unknown:
|
||||
return 'L0Focused';
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MacOSPermissionsService
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Dart-side integration for macOS permission state.
|
||||
///
|
||||
/// Trace: SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091.
|
||||
class MacOSPermissionsService {
|
||||
/// Construct a service bound to [channel]. Injected for testability;
|
||||
/// production code uses the default channel keyed on
|
||||
/// [macOSPermissionsChannelName].
|
||||
MacOSPermissionsService({MethodChannel? channel})
|
||||
: _channel = channel ??
|
||||
(_isMacOS
|
||||
? const MethodChannel(macOSPermissionsChannelName)
|
||||
: null);
|
||||
|
||||
/// Platform-detection seam. Web counts as non-macOS.
|
||||
static bool get _isMacOS {
|
||||
if (kIsWeb) return false;
|
||||
return Platform.isMacOS;
|
||||
}
|
||||
|
||||
final MethodChannel? _channel;
|
||||
bool _started = false;
|
||||
|
||||
// -- Input Monitoring -----------------------------------------------------
|
||||
|
||||
final ValueNotifier<MacOSPermissionState> _inputMonitoringState =
|
||||
ValueNotifier<MacOSPermissionState>(
|
||||
// Non-macOS: granted so consumers are no-ops.
|
||||
_isMacOS
|
||||
? MacOSPermissionState.unknown
|
||||
: MacOSPermissionState.granted,
|
||||
);
|
||||
|
||||
/// Latest known Input Monitoring permission state.
|
||||
///
|
||||
/// On macOS this drives the PTT capability level: `granted` →
|
||||
/// `L1MacOSEventTap` (global PTT via Event Tap); anything else →
|
||||
/// `L0Focused` (focused-only PTT).
|
||||
ValueListenable<MacOSPermissionState> get inputMonitoringState =>
|
||||
_inputMonitoringState;
|
||||
|
||||
// -- Local Network --------------------------------------------------------
|
||||
|
||||
final ValueNotifier<MacOSLocalNetworkState> _localNetworkState =
|
||||
ValueNotifier<MacOSLocalNetworkState>(
|
||||
_isMacOS
|
||||
? MacOSLocalNetworkState.unknown
|
||||
: MacOSLocalNetworkState.unsupported,
|
||||
);
|
||||
|
||||
/// Latest known Local Network permission state.
|
||||
///
|
||||
/// On macOS 15+ (Sequoia) this reflects the Local Network Privacy
|
||||
/// TCC permission. On macOS 14 and earlier, the value is
|
||||
/// [MacOSLocalNetworkState.unsupported] (no prompt needed).
|
||||
ValueListenable<MacOSLocalNetworkState> get localNetworkState =>
|
||||
_localNetworkState;
|
||||
|
||||
// -- Notifications --------------------------------------------------------
|
||||
|
||||
final ValueNotifier<MacOSPermissionState> _notificationState =
|
||||
ValueNotifier<MacOSPermissionState>(
|
||||
_isMacOS
|
||||
? MacOSPermissionState.unknown
|
||||
: MacOSPermissionState.granted,
|
||||
);
|
||||
|
||||
/// Latest known notification authorization state.
|
||||
ValueListenable<MacOSPermissionState> get notificationState =>
|
||||
_notificationState;
|
||||
|
||||
// -- PTT capability (derived) ---------------------------------------------
|
||||
|
||||
final ValueNotifier<String> _pttCapabilityState =
|
||||
ValueNotifier<String>(
|
||||
_pttCapabilityLevel(
|
||||
_isMacOS ? MacOSPermissionState.unknown : MacOSPermissionState.granted,
|
||||
),
|
||||
);
|
||||
|
||||
/// Derived PTT capability level string, ready for consumption by
|
||||
/// [PttCapabilityBadge]. Updates automatically when Input Monitoring
|
||||
/// state changes.
|
||||
///
|
||||
/// Returns `"L1MacOSEventTap"` when Input Monitoring is granted,
|
||||
/// `"L0Focused"` otherwise.
|
||||
ValueListenable<String> get pttCapabilityState => _pttCapabilityState;
|
||||
|
||||
// -- Lifecycle ------------------------------------------------------------
|
||||
|
||||
/// Start listening for state updates from Swift. Idempotent.
|
||||
///
|
||||
/// On non-macOS this is a no-op.
|
||||
///
|
||||
/// Note: this only registers the inbound handler. Call
|
||||
/// [checkInitialStates] afterward to eagerly query the current
|
||||
/// permission states from the native side.
|
||||
void start() {
|
||||
if (_started) return;
|
||||
_started = true;
|
||||
final ch = _channel;
|
||||
if (ch == null) return;
|
||||
ch.setMethodCallHandler(_handle);
|
||||
}
|
||||
|
||||
/// Eagerly query the current permission states from the native
|
||||
/// side. Call this after [start] so the PTT capability badge
|
||||
/// shows the correct level on the first frame.
|
||||
///
|
||||
/// On non-macOS this is a no-op.
|
||||
void checkInitialStates() {
|
||||
final ch = _channel;
|
||||
if (ch == null) return;
|
||||
unawaited(_checkInputMonitoring());
|
||||
unawaited(_checkLocalNetwork());
|
||||
unawaited(_checkNotifications());
|
||||
}
|
||||
|
||||
/// Stop listening. Idempotent.
|
||||
void stop() {
|
||||
if (!_started) return;
|
||||
_started = false;
|
||||
final ch = _channel;
|
||||
if (ch == null) return;
|
||||
ch.setMethodCallHandler(null);
|
||||
}
|
||||
|
||||
// -- Inbound handler (Swift → Dart) --------------------------------------
|
||||
|
||||
Future<dynamic> _handle(MethodCall call) async {
|
||||
switch (call.method) {
|
||||
case methodInputMonitoringStateChanged:
|
||||
final args = call.arguments;
|
||||
if (args is Map) {
|
||||
final state = _parsePermissionState(args['state'] as String?);
|
||||
_inputMonitoringState.value = state;
|
||||
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
||||
}
|
||||
break;
|
||||
case methodLocalNetworkStateChanged:
|
||||
final args = call.arguments;
|
||||
if (args is Map) {
|
||||
_localNetworkState.value =
|
||||
_parseLocalNetworkState(args['state'] as String?);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -- Outbound: Input Monitoring -------------------------------------------
|
||||
|
||||
/// Check the current Input Monitoring permission state without
|
||||
/// triggering a system prompt.
|
||||
Future<MacOSPermissionState> _checkInputMonitoring() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSPermissionState.granted;
|
||||
try {
|
||||
final raw =
|
||||
await ch.invokeMethod<String>(methodCheckInputMonitoring);
|
||||
final state = _parsePermissionState(raw);
|
||||
_inputMonitoringState.value = state;
|
||||
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'checkInputMonitoring failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _inputMonitoringState.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request Input Monitoring permission. On macOS this calls
|
||||
/// `CGRequestListenEventAccess()` which shows a system dialog
|
||||
/// or opens System Settings (depending on macOS version).
|
||||
///
|
||||
/// Returns the new state after the request.
|
||||
Future<MacOSPermissionState> requestInputMonitoring() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSPermissionState.granted;
|
||||
try {
|
||||
final raw = await ch.invokeMethod<String>(
|
||||
methodRequestInputMonitoring,
|
||||
);
|
||||
final state = _parsePermissionState(raw);
|
||||
_inputMonitoringState.value = state;
|
||||
_pttCapabilityState.value = _pttCapabilityLevel(state);
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'requestInputMonitoring failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _inputMonitoringState.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Open System Settings → Privacy & Security → Input Monitoring
|
||||
/// so the user can manually grant the permission for the
|
||||
/// permanently-denied case (TCC drag-based permission cannot be
|
||||
/// programmatically granted).
|
||||
Future<void> openInputMonitoringSettings() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return;
|
||||
try {
|
||||
await ch.invokeMethod<void>(methodOpenInputMonitoringSettings);
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'openInputMonitoringSettings failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// -- Outbound: Local Network ----------------------------------------------
|
||||
|
||||
Future<MacOSLocalNetworkState> _checkLocalNetwork() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSLocalNetworkState.unsupported;
|
||||
try {
|
||||
final raw = await ch.invokeMethod<String>(methodCheckLocalNetwork);
|
||||
final state = _parseLocalNetworkState(raw);
|
||||
_localNetworkState.value = state;
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'checkLocalNetwork failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _localNetworkState.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger the Local Network permission prompt by starting a
|
||||
/// brief `NWBrowser` scan for `_ts3._tcp`. On macOS 15+ this
|
||||
/// shows the system Local Network Privacy dialog. On earlier
|
||||
/// versions, this is a no-op (returns `unsupported`).
|
||||
Future<MacOSLocalNetworkState> triggerLocalNetworkPrompt() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSLocalNetworkState.unsupported;
|
||||
try {
|
||||
final raw = await ch.invokeMethod<String>(
|
||||
methodTriggerLocalNetworkPrompt,
|
||||
);
|
||||
final state = _parseLocalNetworkState(raw);
|
||||
_localNetworkState.value = state;
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'triggerLocalNetworkPrompt failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _localNetworkState.value;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Outbound: Notifications ----------------------------------------------
|
||||
|
||||
Future<MacOSPermissionState> _checkNotifications() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSPermissionState.granted;
|
||||
try {
|
||||
final raw =
|
||||
await ch.invokeMethod<String>(methodCheckNotifications);
|
||||
final state = _parsePermissionState(raw);
|
||||
_notificationState.value = state;
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'checkNotifications failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _notificationState.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Request notification authorization via `UNUserNotificationCenter`.
|
||||
/// Returns the new state after the system dialog resolves.
|
||||
Future<MacOSPermissionState> requestNotifications() async {
|
||||
final ch = _channel;
|
||||
if (ch == null) return MacOSPermissionState.granted;
|
||||
try {
|
||||
final raw = await ch.invokeMethod<String>(
|
||||
methodRequestNotifications,
|
||||
);
|
||||
final state = _parsePermissionState(raw);
|
||||
_notificationState.value = state;
|
||||
return state;
|
||||
} catch (e, st) {
|
||||
developer.log(
|
||||
'requestNotifications failed',
|
||||
name: 'MacOSPermissionsService',
|
||||
error: e,
|
||||
stackTrace: st,
|
||||
);
|
||||
return _notificationState.value;
|
||||
}
|
||||
}
|
||||
|
||||
// -- Cleanup --------------------------------------------------------------
|
||||
|
||||
/// Release state notifiers. Test helper; production keeps the
|
||||
/// service alive for the lifetime of the app.
|
||||
@visibleForTesting
|
||||
void dispose() {
|
||||
stop();
|
||||
_inputMonitoringState.dispose();
|
||||
_localNetworkState.dispose();
|
||||
_notificationState.dispose();
|
||||
_pttCapabilityState.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user