I am able to send an incoming_call notification on both iOS and Android, I'm not actually using it to start a call, it just brings them to a waiting room where I use Agora to
void _listenForNotifications() {
print("Setting up foreground message listener");
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
print('Received a foreground message:');
print('Title: ${message.notification?.title}');
print('Body: ${message.notification?.body}');
print('Data: ${message.data}');
print('Full message dump: $message');
bool isCallNotification = false;
if (message.data['type'] == 'MEETING_STARTED' &&
message.data['notificationType'] == 'incoming_call') {
isCallNotification = true;
print(
"Call detected via MEETING_STARTED type and incoming_call notificationType");
} else if (message.data['notificationType'] == 'incoming_call') {
isCallNotification = true;
print("Call detected via incoming_call notificationType");
} else if (message.data.containsKey('meetingId') &&
message.data.containsKey('channelId') &&
message.data.containsKey('token')) {
isCallNotification = true;
print("Call detected via presence of meeting parameters");
}
if (isCallNotification) {
print("Detected incoming call notification in foreground");
_handleIncomingCall(message);
} else {
// Handle other types of notifications
_handleMessageWhenAppForeground(message);
}
}, onError: (error, stack) {
NotificationLogger.logError("foreground_listener", error, stack);
print("Error in foreground message listener: $error");
print("Stack trace: $stack");
});
print("Foreground message listener set up complete");
}
void _handleIncomingCall(RemoteMessage message) async {
try {
print("[DEBUG] Handling incoming call");
// Use the UUID from the message or generate a new one
final String callId = message.data['uuid'] ?? const Uuid().v4();
// Check for and end any active calls
final List<dynamic> activeCalls =
await FlutterCallkitIncoming.activeCalls();
if (activeCalls.isNotEmpty) {
await FlutterCallkitIncoming.endAllCalls();
}
// Create call parameters
final CallKitParams callKitParams = CallKitParams(
id: callId,
nameCaller: message.data['callerName'] ?? 'Unknown',
appName: 'Foundermatcha',
avatar: message.data['callerImage'],
handle: 'Video Call',
type: 1, // Video call
duration: 30000,
textAccept: 'Accept',
textDecline: 'Decline',
missedCallNotification: const NotificationParams(
showNotification: true,
isShowCallback: true,
subtitle: 'Missed call',
callbackText: 'Call back',
),
extra: {
'meetingId': message.data['meetingId'],
'channelId': message.data['channelId'],
'token': message.data['token'],
'imageRotation': message.data['imageRotation'] ?? '0',
},
android: const AndroidParams(
isCustomNotification: true,
isShowLogo: true,
ringtonePath: 'system_ringtone_default',
backgroundColor: '#78C452',
backgroundUrl: 'assets/images/logo.png',
actionColor: '#4CAF50',
incomingCallNotificationChannelName: "Incoming Call",
missedCallNotificationChannelName: "Missed Call",
),
ios: const IOSParams(
iconName: 'CallKitLogo',
handleType: 'generic',
supportsVideo: true,
maximumCallGroups: 2,
maximumCallsPerCallGroup: 1,
audioSessionMode: 'default',
audioSessionActive: true,
audioSessionPreferredSampleRate: 44100.0,
audioSessionPreferredIOBufferDuration: 0.005,
supportsDTMF: true,
supportsHolding: true,
supportsGrouping: false,
supportsUngrouping: false,
ringtonePath: 'system_ringtone_default',
),
);
print("[DEBUG] Showing call UI");
await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);
print("[DEBUG] Call UI displayed");
// Set up event listener for this specific call
_setupCallEventListener(callId, message);
} catch (e, stack) {
print("[ERROR] Exception in _handleIncomingCall: $e");
print("[ERROR] Stack trace: $stack");
}
}
And when it gets to .showCallkitIncoming it should... Show the call kit! but it always returns null. The system is working for Android but not iOS.
Here's how I start the call:
Future<void> _initiateTestCall() async {
final targetToken = _targetDeviceTokenController.text.trim();
if (targetToken.isEmpty) {
setState(() {
_status = 'Please enter a target device token';
});
return;
}
setState(() {
_isLoading = true;
_status = 'Initiating test call...';
});
try {
final currentUser = ref.read(userProvider);
if (currentUser == null) {
setState(() {
_status = 'Error: Current user not found';
_isLoading = false;
});
return;
}
// Get a sample token for testing
final tokenResult =
await FirebaseFunctions.instance.httpsCallable('generateToken').call({
'channelName': 'test_channel_${DateTime.now().millisecondsSinceEpoch}',
'expiryTime': 3600,
});
final token = tokenResult.data['token'] as String?;
if (token == null) {
setState(() {
_status = 'Error: Failed to generate token';
_isLoading = false;
});
return;
}
final meetingId = "test_meeting_${DateTime.now().millisecondsSinceEpoch}";
final channelId = "test_channel_${DateTime.now().millisecondsSinceEpoch}";
String? callerImage = currentUser.profileImageUrl;
final firebaseMessagingServices =
ref.read(firebaseMessagingServicesProvider);
await firebaseMessagingServices.sendMeetingStartedNotification(
users: [currentUser.uid],
title: "Incoming Video Call",
body: "${currentUser.firstName} is calling you",
type: "MEETING_STARTED",
starterId: currentUser.uid,
//hardcoded
recipientId: "mtIslN60KpWGt1jg1a6jESwSkvW2",
recipientToken: targetToken,
meetingId: meetingId,
channelId: channelId,
token: token,
callerName: currentUser.firstName,
callerImage: callerImage,
notificationType: "incoming_call",
imageNeedsRotation: true,
);
setState(() {
_status = 'Test call notification sent!';
_isLoading = false;
});
} catch (e) {
setState(() {
_status = 'Error sending test call: $e';
_isLoading = false;
});
}
}
Future<void> sendMeetingStartedNotification({
required List<String> users,
required String title,
required String body,
required String type,
required String starterId,
required String recipientId,
String? recipientToken,
String? meetingId,
String? channelId,
String? token,
String? callerName,
String? callerImage,
String? notificationType,
bool imageNeedsRotation = false,
}) async {
print('Sending meeting started notification...');
print('getting token');
print('Recipient token: $recipientToken');
print('Recipient ID: $recipientId');
// Check for missing token
if (recipientToken == null || recipientToken.isEmpty) {
print("Cannot send notification: recipient token is missing");
try {
final userDoc =
await FirebaseConstants.usersCollection.doc(recipientId).get();
recipientToken = userDoc.data()?['deviceToken'];
if (recipientToken == null || recipientToken.isEmpty) {
print("No valid token found for user: $recipientId");
return;
}
} catch (e) {
print("Error fetching token: $e");
return;
}
}
var serverAccessTokenKey = await getAccessToken();
// Generate a UUID for the call that will be consistent between notification and handler
final callUuid = const Uuid().v4();
// Determine channel ID for Android
String androidChannelId = 'high_importance_channel';
if (notificationType == 'incoming_call') {
androidChannelId = 'incoming_calls';
} else if (type == 'MEETING_REMINDER') {
androidChannelId = 'meeting_reminders';
} else if (type.contains('chat') || type.contains('CHAT')) {
androidChannelId = 'support_chat';
}
final Map<String, dynamic> notificationBody;
if (notificationType == 'incoming_call') {
// Data-only message format for calls
notificationBody = {
'message': {
'token': recipientToken,
'data': {
'title': title,
'body': body,
'type': type,
'senderId': starterId,
'recipientId': recipientId,
'meetingId': meetingId ?? 'unknown',
'channelId': channelId ?? 'unknown',
'token': token ?? 'unknown',
'callerName': callerName ?? 'Unknown Caller',
'callerImage': callerImage ?? '',
'imageRotation': imageNeedsRotation ? '270' : '0',
'notificationType': 'incoming_call',
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
'uuid': callUuid,
},
'android': {
'priority': 'high',
},
'apns': {
'headers': {
'apns-priority': '10',
'apns-push-type': 'background',
},
'payload': {
'aps': {
'content-available': 1,
'sound': 'default',
'category': 'INCOMING_CALL',
},
},
},
}
};
print('Call notification body contains: $notificationBody');
} else {
// Regular notification format
notificationBody = {
'message': {
'token': recipientToken,
'notification': {
'title': title,
'body': body,
},
'data': {
'type': type,
'senderId': starterId,
'recipientId': recipientId,
'meetingId': meetingId ?? '',
'channelId': channelId ?? '',
'token': token ?? '',
'callerName': callerName ?? '',
'callerImage': callerImage ?? '',
'imageRotation': imageNeedsRotation ? '270' : '0',
'notificationType': notificationType ?? '',
'timestamp': DateTime.now().millisecondsSinceEpoch.toString(),
},
'android': {
'priority': 'high',
'notification': {
'channel_id': androidChannelId,
},
},
'apns': {
'headers': {
'apns-priority': '10',
},
'payload': {
'aps': {
'category': notificationType == 'incoming_call'
? 'INCOMING_CALL'
: 'DEFAULT',
'sound': 'default',
'badge': 1,
'content-available': 1,
},
},
},
}
};
}
print('Sending notification payload: ${jsonEncode(notificationBody)}');
print('Notification type: $type, Channel: $androidChannelId');
final headers = {
'content-type': 'application/json',
'Authorization': "Bearer $serverAccessTokenKey",
};
try {
final response = await http.post(
Uri.parse(AppConfig.firebaseNotificationsApi),
headers: headers,
body: jsonEncode(notificationBody),
);
if (response.statusCode == 200) {
print('Notification sent successfully - Type: $type');
return;
} else {
print(
'Error sending notification. ${response.statusCode} : ${response.body}');
// Handle token errors
if (response.body.contains("UNREGISTERED") ||
response.statusCode == 404) {
try {
// We already showed the notification for dev environment above
print("Token appears to be invalid - may be a development token");
} catch (e) {
print("Error handling token status: $e");
}
}
// Try alternative format for calls
if (response.statusCode == 400 && notificationType == 'incoming_call') {
print('Attempting alternative call notification format...');
await _sendAlternativeCallNotification(
recipientToken: recipientToken,
title: title,
body: body,
type: type,
starterId: starterId,
recipientId: recipientId,
meetingId: meetingId,
channelId: channelId,
token: token,
callerName: callerName,
callerImage: callerImage,
imageRotation: imageNeedsRotation ? '270' : '0',
serverAccessTokenKey: serverAccessTokenKey,
);
}
}
} catch (e) {
print('Exception sending notification: $e');
}
}
I'm unfamiliar with the the call kit but I can't figure out from the docs what is causing the issue. Here's the terminal out put from the caller:
flutter: Sending meeting started notification...
flutter: getting token
flutter: Recipient token: fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg
flutter: Recipient ID: mtIslN60KpWGt1jg1a6jESwSkvW2
flutter: Call notification body contains: {message: {token: fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg, data: {title: Incoming Video Call, body: 8 is calling you, type: MEETING_STARTED, senderId: 3DaYgDkMtJQo5OmY6cLasygINvh2, recipientId: mtIslN60KpWGt1jg1a6jESwSkvW2, meetingId: test_meeting_1741275230450, channelId: test_channel_1741275230450, token: 007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea, callerName: 8, callerImage: .appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a, imageRotation: 270, notificationType: incoming_call, timestamp: 1741275230707, uuid: e7cca826-29db-480c-9220-ac62f5c71721}, android: {priority: high}, ap<…>
flutter: Sending notification payload: {"message":{"token":"fwzrcggr90A6jFmUUb9M_2:APA91bHL50Ai2WHbppnuw_4LmE50YlyDETfrY2X081A7_QW_-9Aoz8xIQMLyTyKACeeo31VueJvQDttU3Z1PGmmGggre5PdJKkid5B-L0tzY7G7XfkbK3Tg","data":{"title":"Incoming Video Call","body":"8 is calling you","type":"MEETING_STARTED","senderId":"3DaYgDkMtJQo5OmY6cLasygINvh2","recipientId":"mtIslN60KpWGt1jg1a6jESwSkvW2","meetingId":"test_meeting_1741275230450","channelId":"test_channel_1741275230450","token":"007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea","callerName":"8","callerImage":".appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a","imageRotation":"270","notificationType":"incoming_call","timestamp":"1741275230707","uuid":"e7cca826-29db-480c-9220-ac62f5c71721"},<…>
flutter: Notification type: MEETING_STARTED, Channel: incoming_calls
flutter: Notification sent successfully - Type: MEETING_STARTED
And here it is for the receiver(from when the call is received):
flutter: Received a foreground message:
flutter: Title: null
flutter: Body: null
flutter: Data: {body: 8 is calling you, callerImage: .appspot/o/userImages%2F3DaYgDkMtJQo5OmY6cLasygINvh2%2Fprofilepicture?alt=media&token=9810d869-c004-483b-b2a9-d76870c0501a, meetingId: test_meeting_1741275230450, senderId: 3DaYgDkMtJQo5OmY6cLasygINvh2, channelId: test_channel_1741275230450, uuid: e7cca826-29db-480c-9220-ac62f5c71721, timestamp: 1741275230707, imageRotation: 270, callerName: 8, notificationType: incoming_call, type: MEETING_STARTED, title: Incoming Video Call, token: 007eJxTYFAsYD2/tuzmp2trmbyXLJzu55Kx4Mq5zZ6CG0SNxO80/DRXYEg1tUw0tEg2SDVNTDKxSEu1NEtKMkwxsTQwMzZOMTdMjjtwMj3v3Mn0s8VRrIwMjAwsDIwMIMAEJpnBJAuYlGIoSS0uiU/OSMzLS82JNzQ3MTQyNzUysjAyt2RgAAAYCCea, recipientId: mtIslN60KpWGt1jg1a6jESwSkvW2}
flutter: Full message dump: Instance of 'RemoteMessage'
flutter: Call detected via MEETING_STARTED type and incoming_call notificationType
flutter: ✅ Detected incoming call notification in foreground
flutter: [DEBUG] Handling incoming call
flutter: [DEBUG] Showing call UI
flutter: [DEBUG] Call UI displayed
Also I have these background modes in info.plist:
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
<string>processing</string>
</array>
As I'm not actually starting a call I don't believe I need voip And this is my AppDelegate & CallKitBridge
import Flutter
import UIKit
import FirebaseCore
@main
@objc class AppDelegate: FlutterAppDelegate {
private var callKitBridge: SwiftCallKitBridge?
private var methodChannel: FlutterMethodChannel?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
FirebaseApp.configure()
GeneratedPluginRegistrant.register(with: self)
// Initialize CallKit bridge
callKitBridge = SwiftCallKitBridge()
// Setup method channel
if let controller = window?.rootViewController as? FlutterViewController {
methodChannel = FlutterMethodChannel(name: "com.foundermatcha.callkit", binaryMessenger: controller.binaryMessenger)
methodChannel?.setMethodCallHandler({ [weak self] (call, result) in
guard let self = self else { return }
switch call.method {
case "endCall":
self.callKitBridge?.endCall()
result(true)
case "showIncomingCall":
if let args = call.arguments as? [String: Any],
let callerName = args["callerName"] as? String,
let meetingId = args["meetingId"] as? String,
let channelId = args["channelId"] as? String,
let token = args["token"] as? String {
self.callKitBridge?.reportIncomingCall(from: callerName, meetingId: meetingId, channelId: channelId, token: token)
result(true)
} else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid arguments for showIncomingCall", details: nil))
}
default:
result(FlutterMethodNotImplemented)
}
})
// Set method channel on bridge
callKitBridge?.setMethodChannel(methodChannel!)
}
// Register for standard push notifications
UNUserNotificationCenter.current().delegate = self
let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
UNUserNotificationCenter.current().requestAuthorization(
options: authOptions,
completionHandler: { _, _ in }
)
application.registerForRemoteNotifications()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
print("Push token: \(token)")
methodChannel?.invokeMethod("updatePushToken", arguments: ["token": token])
}
// Handle push notifications when app is in background
override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
if let data = userInfo["data"] as? [String: Any],
let type = data["type"] as? String,
type == "incoming_call" {
// Extract call data
let callerName = data["callerName"] as? String ?? "Unknown"
let meetingId = data["meetingId"] as? String ?? ""
let channelId = data["channelId"] as? String ?? ""
let token = data["token"] as? String ?? ""
// Show CallKit UI
callKitBridge?.reportIncomingCall(from: callerName, meetingId: meetingId, channelId: channelId, token: token)
}
completionHandler(.newData)
}
}
import Flutter
import UIKit
import CallKit
import AVFoundation
@objc public class SwiftCallKitBridge: NSObject, CXProviderDelegate {
private let provider: CXProvider
private let callController = CXCallController()
private var callId: UUID?
private var callInfo: [String: Any]?
private var methodChannel: FlutterMethodChannel?
@objc public override init() {
let providerConfiguration = CXProviderConfiguration(localizedName: "Founder Matcha")
providerConfiguration.supportsVideo = true
providerConfiguration.maximumCallsPerCallGroup = 1
providerConfiguration.supportedHandleTypes = [.generic]
providerConfiguration.ringtoneSound = "ringtone_default.caf"
if let iconImage = UIImage(named: "CallKitLogo") {
providerConfiguration.iconTemplateImageData = iconImage.pngData()
}
provider = CXProvider(configuration: providerConfiguration)
super.init()
provider.setDelegate(self, queue: nil)
}
@objc public func setMethodChannel(_ channel: FlutterMethodChannel) {
self.methodChannel = channel
}
// Report incoming call to the system
@objc public func reportIncomingCall(from caller: String, meetingId: String, channelId: String, token: String) {
let callUpdate = CXCallUpdate()
callUpdate.remoteHandle = CXHandle(type: .generic, value: caller)
callUpdate.hasVideo = true
callUpdate.supportsDTMF = false
callUpdate.supportsHolding = false
callUpdate.supportsGrouping = false
callUpdate.supportsUngrouping = false
self.callId = UUID()
// Store call information for later use
self.callInfo = [
"meetingId": meetingId,
"channelId": channelId,
"token": token,
"callerName": caller
]
if let callId = self.callId {
provider.reportNewIncomingCall(with: callId, update: callUpdate) { error in
if let error = error {
print("Failed to report incoming call: \(error.localizedDescription)")
} else {
print("Incoming call reported successfully")
// Enable audio session for the call
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playAndRecord, mode: .voiceChat)
try audioSession.setActive(true)
} catch {
print("Error setting up audio session: \(error)")
}
}
}
}
}
// MARK: - CXProviderDelegate methods
public func providerDidReset(_ provider: CXProvider) {
print("Provider did reset")
callId = nil
callInfo = nil
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(false)
} catch {
print("Error deactivating audio session: \(error)")
}
}
public func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
print("Call answered")
if let callInfo = self.callInfo {
methodChannel?.invokeMethod("callAccepted", arguments: callInfo)
}
action.fulfill()
}
public func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
print("Call ended")
if let callId = self.callId {
methodChannel?.invokeMethod("callEnded", arguments: ["callId": callId.uuidString])
}
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setActive(false)
} catch {
print("Error deactivating audio session: \(error)")
}
callId = nil
callInfo = nil
action.fulfill()
}
// Method to programmatically end the call
@objc public func endCall() {
guard let callId = self.callId else { return }
let endCallAction = CXEndCallAction(call: callId)
let transaction = CXTransaction(action: endCallAction)
callController.request(transaction) { error in
if let error = error {
print("Error ending call: \(error.localizedDescription)")
} else {
print("Call ended successfully")
}
}
}
}
发布者:admin,转转请注明出处:http://www.yc00.com/questions/1744964121a4603569.html
评论列表(0条)