feat(voice): add iOS VAD runtime support

This commit is contained in:
Edison Jwa
2026-05-21 20:51:45 +09:00
parent 171baf6e41
commit 6af4ecab0f
73 changed files with 11529 additions and 1249 deletions
@@ -0,0 +1,114 @@
/// iOS microphone permission integration for AVAudioSession.
///
/// Trace:
/// - SRS-164 (iOS system permission presentation and settings path).
/// - SRS-114 / SRS-138 (iOS platform-service behaviour for audio).
library;
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'android_permissions_service.dart';
@visibleForTesting
const String iosPlatformChannelName = 'chanora/ios_platform';
@visibleForTesting
const String methodGetMicrophonePermissionState =
'getMicrophonePermissionState';
@visibleForTesting
const String methodRequestMicrophonePermission = 'requestMicrophonePermission';
@visibleForTesting
const String methodIosOpenAppSettings = 'openAppSettings';
/// Dart-side integration for iOS microphone permission state.
class IosPermissionsService {
IosPermissionsService({MethodChannel? channel})
: _channel =
channel ??
(_isIOS ? const MethodChannel(iosPlatformChannelName) : null);
static bool get _isIOS {
if (kIsWeb) return false;
return Platform.isIOS;
}
final MethodChannel? _channel;
final ValueNotifier<AndroidRecordAudioPermissionState> _state =
ValueNotifier<AndroidRecordAudioPermissionState>(
_isIOS
? AndroidRecordAudioPermissionState.unknown
: AndroidRecordAudioPermissionState.granted,
);
ValueListenable<AndroidRecordAudioPermissionState> get recordAudioState =>
_state;
Future<void> start() async {
final ch = _channel;
if (ch == null) return;
try {
_state.value = _parseState(
await ch.invokeMethod<String>(methodGetMicrophonePermissionState),
);
} catch (_) {
_state.value = AndroidRecordAudioPermissionState.unknown;
}
}
void stop() {}
Future<AndroidRecordAudioPermissionState> ensureRecordAudio() async {
final ch = _channel;
if (ch == null) return AndroidRecordAudioPermissionState.granted;
try {
final state = _parseState(
await ch.invokeMethod<String>(methodRequestMicrophonePermission),
);
_state.value = state;
return state;
} catch (_) {
return _state.value;
}
}
Future<void> openAppSettings() async {
final ch = _channel;
if (ch == null) return;
try {
await ch.invokeMethod<bool>(methodIosOpenAppSettings);
} catch (e, st) {
developer.log(
'openAppSettings failed',
name: 'IosPermissionsService',
error: e,
stackTrace: st,
);
}
}
@visibleForTesting
void dispose() {
_state.dispose();
}
}
AndroidRecordAudioPermissionState _parseState(String? raw) {
switch (raw) {
case 'Granted':
return AndroidRecordAudioPermissionState.granted;
case 'Denied':
return AndroidRecordAudioPermissionState.permanentlyDenied;
case 'NotDetermined':
return AndroidRecordAudioPermissionState.denied;
default:
return AndroidRecordAudioPermissionState.unknown;
}
}