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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user