feat(voip): add CallKit availability check for iOS devices

This commit is contained in:
cogwheel0
2025-11-26 20:09:34 +05:30
parent c977f028f5
commit 1e841e03f6
4 changed files with 72 additions and 5 deletions

View File

@@ -225,6 +225,6 @@ SPEC CHECKSUMS:
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
PODFILE CHECKSUM: 1f8874f58051a502e2f506dc2d4737781aa1edde PODFILE CHECKSUM: 32adf4606dae7e9ca2351c13b9e3ce1df6ad7ebf
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

View File

@@ -91,6 +91,7 @@
<array> <array>
<string>audio</string> <string>audio</string>
<string>processing</string> <string>processing</string>
<string>voip</string>
</array> </array>
<!-- Background Task Identifiers --> <!-- Background Task Identifiers -->
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>

View File

@@ -1,4 +1,6 @@
import 'dart:developer' as developer; 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/entities/entities.dart';
import 'package:flutter_callkit_incoming/flutter_callkit_incoming.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. /// Thin wrapper around `flutter_callkit_incoming` for voice calls.
class CallKitService { class CallKitService {
CallKitService({Uuid? uuid}) : _uuid = uuid ?? const Uuid(); CallKitService({Uuid? uuid})
: _uuid = uuid ?? const Uuid(),
_callKitAllowed = _computeCallKitAllowed();
final Uuid _uuid; final Uuid _uuid;
final bool _callKitAllowed;
bool _loggedCallKitDisabled = false;
static const int _defaultCallDurationMs = 2 * 60 * 60 * 1000; // 2 hours 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. /// Requests the notification/full-screen intent permissions needed on Android.
Future<void> requestPermissions() async { Future<void> requestPermissions() async {
if (!_shouldUseCallKit('request permissions')) return;
await _safe( await _safe(
() => FlutterCallkitIncoming.requestNotificationPermission( () => FlutterCallkitIncoming.requestNotificationPermission(
<String, dynamic>{ <String, dynamic>{
@@ -37,6 +48,8 @@ class CallKitService {
String? avatar, String? avatar,
int? durationMs, int? durationMs,
}) async { }) async {
if (!_shouldUseCallKit('start call')) return null;
final id = _uuid.v4(); final id = _uuid.v4();
final params = _buildParams( final params = _buildParams(
id: id, id: id,
@@ -61,6 +74,8 @@ class CallKitService {
/// Marks the current call as connected so iOS shows an incrementing timer. /// Marks the current call as connected so iOS shows an incrementing timer.
Future<void> markCallConnected(String id) async { Future<void> markCallConnected(String id) async {
if (!_shouldUseCallKit('mark call connected')) return;
try { try {
await FlutterCallkitIncoming.setCallConnected(id); await FlutterCallkitIncoming.setCallConnected(id);
} catch (error, stackTrace) { } catch (error, stackTrace) {
@@ -75,16 +90,22 @@ class CallKitService {
/// Ends a specific call id. /// Ends a specific call id.
Future<void> endCall(String id) async { Future<void> endCall(String id) async {
if (!_shouldUseCallKit('end call')) return;
await _safe(() => FlutterCallkitIncoming.endCall(id)); await _safe(() => FlutterCallkitIncoming.endCall(id));
} }
/// Clears all ongoing/missed calls. /// Clears all ongoing/missed calls.
Future<void> endAllCalls() async { Future<void> endAllCalls() async {
if (!_shouldUseCallKit('end all calls')) return;
await _safe(FlutterCallkitIncoming.endAllCalls); await _safe(FlutterCallkitIncoming.endAllCalls);
} }
/// Returns the platform VOIP token (iOS PushKit) when available. /// Returns the platform VOIP token (iOS PushKit) when available.
Future<String?> getVoipToken() async { Future<String?> getVoipToken() async {
if (!_shouldUseCallKit('fetch VoIP token')) return null;
final token = await _safe<dynamic>( final token = await _safe<dynamic>(
() => FlutterCallkitIncoming.getDevicePushTokenVoIP(), () => FlutterCallkitIncoming.getDevicePushTokenVoIP(),
); );
@@ -95,6 +116,10 @@ class CallKitService {
/// Returns the raw active call list from the plugin. /// Returns the raw active call list from the plugin.
Future<List<Map<String, dynamic>>> activeCalls() async { Future<List<Map<String, dynamic>>> activeCalls() async {
if (!_shouldUseCallKit('fetch active calls')) {
return <Map<String, dynamic>>[];
}
final calls = await _safe<dynamic>(FlutterCallkitIncoming.activeCalls); final calls = await _safe<dynamic>(FlutterCallkitIncoming.activeCalls);
if (calls is List) { if (calls is List) {
return calls return calls
@@ -106,8 +131,14 @@ class CallKitService {
} }
/// Stream of CallKit events from the native layer. /// Stream of CallKit events from the native layer.
Stream<CallEvent> get events => Stream<CallEvent> get events {
FlutterCallkitIncoming.onEvent.where((event) => event != null).cast(); if (!_callKitAllowed) {
return const Stream<CallEvent>.empty();
}
return FlutterCallkitIncoming.onEvent
.where((event) => event != null)
.cast();
}
CallKitParams _buildParams({ CallKitParams _buildParams({
required String id, required String id,
@@ -177,6 +208,32 @@ class CallKitService {
return null; 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) @Riverpod(keepAlive: true)

View File

@@ -71,6 +71,7 @@ class VoiceCallService {
bool _callKitPermissionsRequested = false; bool _callKitPermissionsRequested = false;
String? _callKitCallId; String? _callKitCallId;
bool _callKitConnectedReported = false; bool _callKitConnectedReported = false;
bool get _callKitEnabled => _callKitService.isAvailable;
final StreamController<VoiceCallState> _stateController = final StreamController<VoiceCallState> _stateController =
StreamController<VoiceCallState>.broadcast(); StreamController<VoiceCallState>.broadcast();
@@ -172,12 +173,16 @@ class VoiceCallService {
} }
Future<void> _ensureCallKitPermissions() async { Future<void> _ensureCallKitPermissions() async {
if (!_callKitEnabled) return;
if (_callKitPermissionsRequested) return; if (_callKitPermissionsRequested) return;
_callKitPermissionsRequested = true; _callKitPermissionsRequested = true;
await _callKitService.requestPermissions(); await _callKitService.requestPermissions();
} }
Future<void> _startCallKitSession({required String modelName}) async { Future<void> _startCallKitSession({required String modelName}) async {
if (!_callKitEnabled) return;
try { try {
await _ensureCallKitPermissions(); await _ensureCallKitPermissions();
final callId = await _callKitService.startOutgoingVoiceCall( final callId = await _callKitService.startOutgoingVoiceCall(
@@ -197,6 +202,8 @@ class VoiceCallService {
} }
Future<void> _endCallKitSession() async { Future<void> _endCallKitSession() async {
if (!_callKitEnabled) return;
if (_callKitCallId == null) { if (_callKitCallId == null) {
return; return;
} }
@@ -218,7 +225,7 @@ class VoiceCallService {
void _listenForCallKitEvents() { void _listenForCallKitEvents() {
_callKitEventSubscription?.cancel(); _callKitEventSubscription?.cancel();
if (_callKitCallId == null) return; if (!_callKitEnabled || _callKitCallId == null) return;
_callKitEventSubscription = _callKitService.events.listen((callEvent) { _callKitEventSubscription = _callKitService.events.listen((callEvent) {
final eventId = _extractCallId(callEvent.body); final eventId = _extractCallId(callEvent.body);
@@ -268,6 +275,8 @@ class VoiceCallService {
} }
Future<void> _markCallKitConnected() async { Future<void> _markCallKitConnected() async {
if (!_callKitEnabled) return;
if (_callKitCallId == null || _callKitConnectedReported) return; if (_callKitCallId == null || _callKitConnectedReported) return;
await _callKitService.markCallConnected(_callKitCallId!); await _callKitService.markCallConnected(_callKitCallId!);
_callKitConnectedReported = true; _callKitConnectedReported = true;