diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8c338a7..1365e15 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -225,6 +225,6 @@ SPEC CHECKSUMS: wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d -PODFILE CHECKSUM: 1f8874f58051a502e2f506dc2d4737781aa1edde +PODFILE CHECKSUM: 32adf4606dae7e9ca2351c13b9e3ce1df6ad7ebf COCOAPODS: 1.16.2 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index ff92f4b..7b72d23 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -91,6 +91,7 @@ audio processing + voip BGTaskSchedulerPermittedIdentifiers diff --git a/lib/core/services/callkit_service.dart b/lib/core/services/callkit_service.dart index 3334a8b..61c452f 100644 --- a/lib/core/services/callkit_service.dart +++ b/lib/core/services/callkit_service.dart @@ -1,4 +1,6 @@ import 'dart:developer' as developer; +import 'dart:io'; +import 'dart:ui' as ui; import 'package:flutter_callkit_incoming/entities/entities.dart'; import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart'; @@ -9,13 +11,22 @@ part 'callkit_service.g.dart'; /// Thin wrapper around `flutter_callkit_incoming` for voice calls. class CallKitService { - CallKitService({Uuid? uuid}) : _uuid = uuid ?? const Uuid(); + CallKitService({Uuid? uuid}) + : _uuid = uuid ?? const Uuid(), + _callKitAllowed = _computeCallKitAllowed(); final Uuid _uuid; + final bool _callKitAllowed; + bool _loggedCallKitDisabled = false; static const int _defaultCallDurationMs = 2 * 60 * 60 * 1000; // 2 hours + /// Returns whether CallKit can be used on this device/region. + bool get isAvailable => _callKitAllowed; + /// Requests the notification/full-screen intent permissions needed on Android. Future requestPermissions() async { + if (!_shouldUseCallKit('request permissions')) return; + await _safe( () => FlutterCallkitIncoming.requestNotificationPermission( { @@ -37,6 +48,8 @@ class CallKitService { String? avatar, int? durationMs, }) async { + if (!_shouldUseCallKit('start call')) return null; + final id = _uuid.v4(); final params = _buildParams( id: id, @@ -61,6 +74,8 @@ class CallKitService { /// Marks the current call as connected so iOS shows an incrementing timer. Future markCallConnected(String id) async { + if (!_shouldUseCallKit('mark call connected')) return; + try { await FlutterCallkitIncoming.setCallConnected(id); } catch (error, stackTrace) { @@ -75,16 +90,22 @@ class CallKitService { /// Ends a specific call id. Future endCall(String id) async { + if (!_shouldUseCallKit('end call')) return; + await _safe(() => FlutterCallkitIncoming.endCall(id)); } /// Clears all ongoing/missed calls. Future endAllCalls() async { + if (!_shouldUseCallKit('end all calls')) return; + await _safe(FlutterCallkitIncoming.endAllCalls); } /// Returns the platform VOIP token (iOS PushKit) when available. Future getVoipToken() async { + if (!_shouldUseCallKit('fetch VoIP token')) return null; + final token = await _safe( () => FlutterCallkitIncoming.getDevicePushTokenVoIP(), ); @@ -95,6 +116,10 @@ class CallKitService { /// Returns the raw active call list from the plugin. Future>> activeCalls() async { + if (!_shouldUseCallKit('fetch active calls')) { + return >[]; + } + final calls = await _safe(FlutterCallkitIncoming.activeCalls); if (calls is List) { return calls @@ -106,8 +131,14 @@ class CallKitService { } /// Stream of CallKit events from the native layer. - Stream get events => - FlutterCallkitIncoming.onEvent.where((event) => event != null).cast(); + Stream get events { + if (!_callKitAllowed) { + return const Stream.empty(); + } + return FlutterCallkitIncoming.onEvent + .where((event) => event != null) + .cast(); + } CallKitParams _buildParams({ required String id, @@ -177,6 +208,32 @@ class CallKitService { return null; } } + + bool _shouldUseCallKit(String reason) { + if (_callKitAllowed) return true; + if (_loggedCallKitDisabled) return false; + _loggedCallKitDisabled = true; + developer.log( + 'CallKit disabled on iOS devices set to mainland China; ' + 'skipping $reason.', + name: 'callkit', + ); + return false; + } + + static bool _computeCallKitAllowed() { + if (!Platform.isIOS) return true; + + final dispatcher = ui.PlatformDispatcher.instance; + final locale = dispatcher.locale; + return !_isMainlandChinaLocale(locale); + } + + static bool _isMainlandChinaLocale(ui.Locale? locale) { + if (locale == null) return false; + final country = locale.countryCode?.toUpperCase(); + return country == 'CN'; + } } @Riverpod(keepAlive: true) diff --git a/lib/features/chat/services/voice_call_service.dart b/lib/features/chat/services/voice_call_service.dart index fb672bc..d53912c 100644 --- a/lib/features/chat/services/voice_call_service.dart +++ b/lib/features/chat/services/voice_call_service.dart @@ -71,6 +71,7 @@ class VoiceCallService { bool _callKitPermissionsRequested = false; String? _callKitCallId; bool _callKitConnectedReported = false; + bool get _callKitEnabled => _callKitService.isAvailable; final StreamController _stateController = StreamController.broadcast(); @@ -172,12 +173,16 @@ class VoiceCallService { } Future _ensureCallKitPermissions() async { + if (!_callKitEnabled) return; + if (_callKitPermissionsRequested) return; _callKitPermissionsRequested = true; await _callKitService.requestPermissions(); } Future _startCallKitSession({required String modelName}) async { + if (!_callKitEnabled) return; + try { await _ensureCallKitPermissions(); final callId = await _callKitService.startOutgoingVoiceCall( @@ -197,6 +202,8 @@ class VoiceCallService { } Future _endCallKitSession() async { + if (!_callKitEnabled) return; + if (_callKitCallId == null) { return; } @@ -218,7 +225,7 @@ class VoiceCallService { void _listenForCallKitEvents() { _callKitEventSubscription?.cancel(); - if (_callKitCallId == null) return; + if (!_callKitEnabled || _callKitCallId == null) return; _callKitEventSubscription = _callKitService.events.listen((callEvent) { final eventId = _extractCallId(callEvent.body); @@ -268,6 +275,8 @@ class VoiceCallService { } Future _markCallKitConnected() async { + if (!_callKitEnabled) return; + if (_callKitCallId == null || _callKitConnectedReported) return; await _callKitService.markCallConnected(_callKitCallId!); _callKitConnectedReported = true;