Merge pull request #180 from cogwheel0/voip-callkit-availability-check
feat(voip): add CallKit availability check for iOS devices
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user