hiennguyen92 / flutter_callkit_incoming

Flutter Callkit Incoming
https://pub.dev/packages/flutter_callkit_incoming
MIT License
169 stars 271 forks source link

I just want to thank you for this awesome package. #141

Closed Musfick closed 2 years ago

Musfick commented 2 years ago

I successfully implemented calling feature on ios using firebase FCM. It works in every state, Background state, Foreground state, Terminated state.

Thank You again ❤️

ycherniavskyi commented 2 years ago

Hm on iOS to get CallKit integration you need to use PushKit, and as I know FCM can't sent such type of push messages 🤔.

Musfick commented 2 years ago

I have done it by only using FCM and some ios configuration. FCM is working fine in my case for receive calls and everything. Also i am using agora for video and audio call.

ycherniavskyi commented 2 years ago

Yes sure FCM will work. But as I understand in such case you will see regular push notification message when app in background/terminated. Or you somehow call CallKit from regular push notification handler (but it will executed as I understand only on tap)?

zionnite commented 2 years ago

Good afternoon @Musfick can you please walk me through how you made it work on ios, have literally tried everything and am out of options, its working perfectly well for my android

am using FCM and this package for this. +2349034286339 (WhatsApp and Telegram) please lets get connected

please am humble to learn from you @Musfick

waqadArshad commented 2 years ago

@Musfick Hi Musfick, I wanted to ask something regarding navigation on ACCEPT event. I am stuck at this. I am using Firebase Push Notifications. Any help is appreciated.

Thank u

zionnite commented 2 years ago

@Musfick Hi Musfick, I wanted to ask something regarding navigation on ACCEPT event. I am stuck at this. I am using Firebase Push Notifications. Any help is appreciated.

Thank u

whats your challenge?

waqadArshad commented 2 years ago

@zionnite Thanks a lot for getting back to me.

This is my background handler.

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  bool videoCallEnabled = false;
  bool audioCallEnabled = false;

  if (message != null) {
    debugPrint("Handling background is called");
    print("Handling a background message and background handler: ${message.messageId}");
    try {
      videoCallEnabled = message.data.containsKey('videoCall');
      audioCallEnabled = message.data.containsKey('voiceCall');

      if (videoCallEnabled || audioCallEnabled) {
        log("Video call is configured and is started");
        showCallkitIncoming(Uuid().v4(), message: message);
        //w8 for streaming
        debugPrint("Should listen to events");
        _NikkahMatchState.listenerEvent(message);
      } else {
        log("No Video or audio call was initialized");
      }
    } catch (e) {
      debugPrint("Error occured:" + e.toString());
    }
  }
}

And this is my Listener event ACCEPT case:

case CallEvent.ACTION_CALL_ACCEPT:
            // TODO: accepted an incoming call
            // TODO: show screen calling in Flutter
            log("Call Accepted onMessageOpened state");
            var ccurrentChannel = chatRoomId;
            currentChannel = chatRoomId;
            log("currentChannel in accepted is: $ccurrentChannel");
            log("currentChannel in accepted is: $currentChannel");
            debugPrint("Details of call" + chatRoomId + callsDocId);
            CallDocModel passableAbleCdm = CallDocModel();
            await FirebaseFirestore.instance
                .collection("ChatRoom")
                .doc(chatRoomId)
                .collection("calls")
                .doc(callsDocId)
                .update({'receiverCallResponse': 'Accepted', 'callResponseDateTime': FieldValue.serverTimestamp()});

            if (videoCallEnabled) {
              log("in video call enabled in accept call of listener event");
              videoCallIsActive = "Yes";

              /*NavigationService().navigationKey.currentState.push(
                MaterialPageRoute(builder: (_) => VideoCallAgoraUIKit(
                  anotherUserName: callerName,
                  anotherUserImage: imageUrl,
                  channelName: chatRoomId,
                  token: "",
                  anotherUserId: "",
                  docId: callsDocId,
                ),));*/
              FirebaseFirestore.instance
                  .collection("ChatRoom")
                  .doc(currentChannel)
                  .collection("calls")
                  .doc(callsDocId)
                  .get()
                  .then((value) async {
                passableAbleCdm = CallDocModel.fromJson(value.data());
                backgroundPassableAbleCdm = passableAbleCdm;
              });
              videoCallIsActive = "Yes";

              log("${DateTime.now()} : before updating user value");
              await ffstore.collection("Users").doc(auth.currentUser.uid).update({"isOnCall": true});
              log("${DateTime.now()}  after updating user value");
              // await NavigationService.instance.pushNamed(
              //   AppRoute.makingVideoCall,
              // );

              // await initialization.then((value) async {
              //   // Get.put(AppController());
              //   await di.init();
              //   Future.delayed(Duration(seconds: 20), () {
              //     authController.navigateToSuitableScreen();
              //   });
              //   // FirebaseDynamicLinkService.initDynamicLink();
              // });
                // NavigationService().navigationKey.currentState.push( MaterialPageRoute(
                //      builder: (context) => VideoCallAgoraUIKit(
                //        anotherUserName: requesterName,
                //        anotherUserImage: requesterImageUrl,
                //        channelName: chatRoomId,
                //        token: "",
                //        anotherUserId: "",
                //        docId: callsDocId,
                //        callDoc: passableAbleCdm,
                //      ),));
              currentChannel = chatRoomId;
              log("this is where we are navigating wth currentChannel: $currentChannel and chatRoomId: $chatRoomId");
              // Get.to(
              //   () => VideoCallAgoraUIKit(
              //     anotherUserName: requesterName,
              //     anotherUserImage: requesterImageUrl,
              //     channelName: chatRoomId,
              //     token: "",
              //     anotherUserId: "",
              //     docId: callsDocId,
              //     callDoc: passableAbleCdm,
              //   ),
              // );
            } else {
              log("in voice call enabled in accept call of listener event");
              var voiceCallTokenn = await GetToken().getTokenMethod(backgroundChatRoomId, auth.currentUser.uid);
              log("token before if in splashscreen is: $voiceCallToken");
              if (voiceCallTokenn != null) {
                FirebaseFirestore.instance
                    .collection("ChatRoom")
                    .doc(currentChannel)
                    .collection("calls")
                    .doc(callsDocId)
                    .get()
                    .then((value) async {
                  log('after fetching the doc passableAbleCdm in splashscreen');
                  CallDocModel passableAbleCdm = CallDocModel.fromJson(value.data());
                  backgroundPassableAbleCdm = passableAbleCdm;
                  Get.to(
                    () => VoiceCall(
                      toCallName: requesterName,
                      toCallImageUrl: requesterImageUrl,
                      channelName: chatRoomId,
                      token: voiceCallTokenn,
                      docId: callsDocId,
                      callDoc: passableAbleCdm,
                    ),
                  );
                });
              } else {
                log("in token being null in voice call enabled in accept call of listener event");
              }
            }

            break;

You may notice the comments that show that I have tried Get as I am using GetX and also tried passing a navigator key which in this background case returns currentState as null.

zionnite commented 2 years ago

You know what mate,

I will take a look at it tomorrow noon it's already late I will need to open my system to take a look at it

waqadArshad commented 2 years ago

no issues and thanks a bunch. I appreciate that.

waqadArshad commented 2 years ago

You know what mate,

I will take a look at it tomorrow noon it's already late I will need to open my system to take a look at it

@zionnite Still waiting brother. I would really appreciate a response. thank u. We can also connect via Whatsapp at +923064278613.

zionnite commented 2 years ago

I replied one of ur post, check that out

Musfick commented 2 years ago

My FirebaseMessagingBackgroundHandler

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
    await GetStorage.init();
    await FirebaseService().init();
    await NotificationService().init();

    final service =  MeetingService();
    await service.initCallKitCallBack(isTerminated: true);
    debugPrint("Handling a background message: ${message.messageId}");
    final meetingState = MeetingState.fromJson(message.data);
    switch (meetingState.type) {
      case FCM_TYPE_NOTIFICATION:
        NotificationService()
            .showNotification(message.data["title"], message.data["message"]);
        break;
      case RECEIVE_VIDEO_MEETING_INVITATION:
        service.startIncomingCall(meetingState);
        break;
      case CANCELED_VIDEO_MEETING_INVITATION:
        service.endCurrentCall();
        break;
    }
  }

Callkit Callback

initCallKitCallBack({bool isTerminated = false}) async {
    try {
      FlutterCallkitIncoming.onEvent.listen((event) async {
        switch (event?.name) {
          case CallEvent.ACTION_CALL_ACCEPT:
            debugPrint("ACTION_CALL_ACCEPT");
            if(isTerminated)return;
            acceptCall(event?.body["extra"]);
            break;
          case CallEvent.ACTION_CALL_DECLINE:
            debugPrint("ACTION_CALL_DECLINE");
            var meetingState = MeetingState.fromJson(event?.body["extra"]);
            if (!isCallCanceled) {
              sendPush(DECLINE_VIDEO_MEETING_INVITATION, meetingState);
            }
            break;
        }
      });
    } on Exception {}
  }

Navigate to Screen

  acceptCall(extra){
    endCurrentCall();
    var meetingState = MeetingState.fromJson(extra);
    sendPush(ACCEPT_VIDEO_MEETING_INVITATION, meetingState);
    Get.to(
            () => MeetingPage(),
        arguments: {
          "agora_channel": ________,
        });
  }

App On Start

When app is in terminated state check if user is in current call. Better understand check this package example

  @override
  void onInit() {
    checkCurrentCall();
    super.onInit();
  }

  void checkCurrentCall() async {
    final list = await FlutterCallkitIncoming.activeCalls();
    if(list.length != 0){
      final extra = box.read("call_extra");
      MeetingService().acceptCall(extra);
    }
  }
waqadArshad commented 2 years ago

@Musfick where did you put this acceptCall method because I have this method in the ACTION_CALL_ACCEPT event and I am getting the

[navigatorState is null when using pushNamed Navigation onGenerateRoutes of GetMaterialPage](https://stackoverflow.com/questions/72845544/navigatorstate-is-null-when-using-pushnamed-navigation-ongenerateroutes-of-getma)

for the navigator key in the background and terminated state. (but the navigator key is working correctly in the foreground ) and for the getx, I am getting something that asks me to put GetMaterialApp in place of the MaterialApp, which is already there but probably haven't been initialized yet.

billthecoder046 commented 2 years ago

@Musfick Can you please share any github repository where we can look deep into it... We need you!

Musfick commented 2 years ago

@Musfick where did you put this acceptCall method because I have this method in the ACTION_CALL_ACCEPT event and I am getting the

[navigatorState is null when using pushNamed Navigation onGenerateRoutes of GetMaterialPage](https://stackoverflow.com/questions/72845544/navigatorstate-is-null-when-using-pushnamed-navigation-ongenerateroutes-of-getma)

for the navigator key in the background and terminated state. (but the navigator key is working correctly in the foreground ) and for the getx, I am getting something that asks me to put GetMaterialApp in place of the MaterialApp, which is already there but probably haven't been initialized yet.

when the app is in terminated state don't navigate to screen from callback. When user accept the call from terminated state it basically open the app but the it keep the call active. After open the app check if any active call is running..if active then navigate to Screen. For better understand please take a look this example https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/lib/main.dart

Important

  @override
  void initState() {
    super.initState();
    _uuid = Uuid();
    initFirebase();
    WidgetsBinding.instance?.addObserver(this);
    //Check call when open app from terminated
    checkAndNavigationCallingPage();
  }

  getCurrentCall() async {
    //check current call from pushkit if possible
    var calls = await FlutterCallkitIncoming.activeCalls();
    if (calls is List) {
      if (calls.isNotEmpty) {
        print('DATA: $calls');
        this._currentUuid = calls[0]['id'];
        return calls[0];
      } else {
        this._currentUuid = "";
        return null;
      }
    }
  }

  checkAndNavigationCallingPage() async {
    var currentCall = await getCurrentCall();
    if (currentCall != null) {
      NavigationService.instance
          .pushNamedIfNotCurrent(AppRoute.callingPage, args: currentCall);
    }
  }
Musfick commented 2 years ago

@Musfick Can you please share any github repository where we can look deep into it... We need you!

Best Example

https://github.com/hiennguyen92/flutter_callkit_incoming/tree/master/example/lib

bihim commented 2 years ago

I am having trouble in getting any call when app is terminated state. The calls are coming when my app is in background or lock state. What are the right procedure to get any call when the app is terminated? I am using firebase cloud functions. Any kind of help is much appreciated.

billthecoder046 commented 2 years ago

I am having trouble in getting any call when app is terminated state. The calls are coming when my app is in background or lock state. What are the right procedure to get any call when the app is terminated? I am using firebase cloud functions. Any kind of help is much appreciated.

@bihim in Android or iOS?

bihim commented 2 years ago

I am having trouble in getting any call when app is terminated state. The calls are coming when my app is in background or lock state. What are the right procedure to get any call when the app is terminated? I am using firebase cloud functions. Any kind of help is much appreciated.

@bihim in Android or iOS?

Oh sorry to mention. In iOS.

billthecoder046 commented 2 years ago

I'm having the same issue. it is because of the iOS settings. I have read somewhere that if we can somehow successfully implement pushKit for iOS, we might be able to get the device to wake up on call and show callKit notifications with the help of that. And I also tried that but I couldn't get it to work. The notifications are received for about 10-15 minutes after terminating the app even without the pushKit configuration (which I did and am not sure whether it was correct or not because it didn't work). And after those 10-15 minutes, I have to re-open the app and get the 10-15 minute active cycle of call notifications.

Though I should mention that the text message notifications are working even in terminated state.

billthecoder046 commented 2 years ago

I am having trouble in getting any call when app is terminated state. The calls are coming when my app is in background or lock state. What are the right procedure to get any call when the app is terminated? I am using firebase cloud functions. Any kind of help is much appreciated.

@bihim in Android or iOS?

Oh sorry to mention. In iOS.

@Musfick @ycherniavskyi @zionnite Can any of you please help in regards to getting the call notifications on iOS even in terminated state? Thanks

Musfick commented 2 years ago

I am having trouble in getting any call when app is terminated state. The calls are coming when my app is in background or lock state. What are the right procedure to get any call when the app is terminated? I am using firebase cloud functions. Any kind of help is much appreciated.

@bihim in Android or iOS?

Oh sorry to mention. In iOS.

@Musfick @ycherniavskyi @zionnite Can any of you please help in regards to getting the call notifications on iOS even in terminated state? Thanks

I have implemented callkit with only FCM. I have not use pushkit.

Note: This function execute only in for foreground mode

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print('Got a message whilst in the foreground!');
  print('Message data: ${message.data}');

  if (message.notification != null) {
    print('Message also contained a notification: ${message.notification}');
  }
});

Note: This function execute when app is in terminated state and background state

Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // If you're going to use other Firebase services in the background, such as Firestore,
  // make sure you call `initializeApp` before using other Firebase services.
  await Firebase.initializeApp();

  print("Handling a background message: ${message.messageId}");
}

void main() {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(MyApp());
}
waqadArshad commented 2 years ago

@Musfick does this function keep executing even after a lot of time have passed after termination?

Musfick commented 2 years ago

@Musfick does this function keep executing even after a lot of time have passed after termination?

no

billthecoder046 commented 2 years ago

@Musfick does this function keep executing even after a lot of time have passed after termination?

no

@Musfick and you are ok with that? I mean I am making a project for a client and cannot find a solution to this problem. There must be a way to wake the iOS app from terminated state anytime, so that showCallKitIncoming can be called. Do you have any suggestions? @hiennguyen92 @zionnite can u please help with this? Thanks

Musfick commented 2 years ago

@Musfick does this function keep executing even after a lot of time have passed after termination?

no

@Musfick and you are ok with that? I mean I am making a project for a client and cannot find a solution to this problem. There must be a way to wake the iOS app from terminated state anytime, so that showCallKitIncoming can be called. Do you have any suggestions? @hiennguyen92 @zionnite can u please help with this? Thanks

When one use call another use, my app get instant call. Whatever the app in background, foreground, terminated state. Like when you post a notification using fcm, phone receive the notification instantly no matter the app is in which state. I have only used fcm to implement the feature. I dont need to use pushKit for this.

waqadArshad commented 2 years ago

@Musfick May I see the notification payload? I mean the whole sed method? because I am sending a silent notification and it stops getting through after some time. But the other notifications are being received.

Musfick commented 2 years ago

@Musfick May I see the notification payload? I mean the whole sed method? because I am sending a silent notification and it stops getting through after some time. But the other notifications are being received.

Sending Call Invitation Payload

final params = {
          "content_available": true,
          "data": {
            "meeting_token": data["agora_data"]["token"],
            "message": type == "paid"
                ? "Meeting : ${bookedAppointment.startTime} - ${bookedAppointment.endTime}"
                : "Free Meeting Invitation",
            "meeting_id": bookedAppointment.bookingSlotId.toString(),
            "type": SEND_VIDEO_MEETING_INVITATION,
            "meeting_type": type,
            "from_token": myToke,
            "meeting_channel": data["agora_data"]["channel_name"],
            "remaining_time": data["details"]
                ["remaining_time_in_real_millisecond"],
            "call_Type": medium,
            "profile_image": profile.value?.profileImage,
            "duration": bookedAppointment.duration,
            "duration_in": bookedAppointment.duration,
            "designation":
                profile.value?.counsellorProfessionalDetails?.profession,
            "name": profile.value?.name,
            "sender_id": profile.value?.userId
          },
          "to": tokenRes["data"]["token"]
        };
waqadArshad commented 2 years ago

@Musfick this params part is inside the apns section? Because here's mine:

await admin.messaging().send({
    token: token_o,
    notification: {},
    data: {
      imageUrl: requesterImageUrl,
      chatRoomId: chatRoomId,
      screenName: 'voiceScreen',
      voiceCall: 'voiceCall',
      callerName: requesterName,
      callsDocId: callsDocId,
      senderId: requesterId,
      callInitTime:callInitTime
    },
        android: {
          notification: {
            click_action: "android.intent.action.MAIN"
          },
        },
    apns: {
      headers: {
        "apns-push-type": "alert",
        "apns-priority": "5", // Must be `5` when `contentAvailable` is set to true.
      },
      payload: {
        aps: {
          alert: {},
          badge: 0,
          contentAvailable: true,
        },
        notification: {
          //ti `You received a call from  ${requesterName}`,
          //body: "",
          //imageUrl: requesterImageUrl,
        },
        priority: "high",
      },
    }
  }).then(value => {
    functions.logger.log("Notification for AudioCall is sent to the Receiver");
  }).catch((e) => {
    functions.logger.log(e.toString());
  });
Musfick commented 2 years ago

@Musfick May I see the notification payload? I mean the whole sed method? because I am sending a silent notification and it stops getting through after some time. But the other notifications are being received.

Sending Call Invitation Payload

final params = {
          "content_available": true,
          "data": {
            "meeting_token": data["agora_data"]["token"],
            "message": type == "paid"
                ? "Meeting : ${bookedAppointment.startTime} - ${bookedAppointment.endTime}"
                : "Free Meeting Invitation",
            "meeting_id": bookedAppointment.bookingSlotId.toString(),
            "type": SEND_VIDEO_MEETING_INVITATION,
            "meeting_type": type,
            "from_token": myToke,
            "meeting_channel": data["agora_data"]["channel_name"],
            "remaining_time": data["details"]
                ["remaining_time_in_real_millisecond"],
            "call_Type": medium,
            "profile_image": profile.value?.profileImage,
            "duration": bookedAppointment.duration,
            "duration_in": bookedAppointment.duration,
            "designation":
                profile.value?.counsellorProfessionalDetails?.profession,
            "name": profile.value?.name,
            "sender_id": profile.value?.userId
          },
          "to": tokenRes["data"]["token"]
        };

I am hitting this https://fcm.googleapis.com/fcm/send url with this payload.

waqadArshad commented 2 years ago

@Musfick this params part is inside the apns section? Because here's mine:

await admin.messaging().send({
    token: token_o,
    notification: {},
    data: {
      imageUrl: requesterImageUrl,
      chatRoomId: chatRoomId,
      screenName: 'voiceScreen',
      voiceCall: 'voiceCall',
      callerName: requesterName,
      callsDocId: callsDocId,
      senderId: requesterId,
      callInitTime:callInitTime
    },
        android: {
          notification: {
            click_action: "android.intent.action.MAIN"
          },
        },
    apns: {
      headers: {
        "apns-push-type": "alert",
        "apns-priority": "5", // Must be `5` when `contentAvailable` is set to true.
      },
      payload: {
        aps: {
          alert: {},
          badge: 0,
          contentAvailable: true,
        },
        notification: {
          //ti `You received a call from  ${requesterName}`,
          //body: "",
          //imageUrl: requesterImageUrl,
        },
        priority: "high",
      },
    }
  }).then(value => {
    functions.logger.log("Notification for AudioCall is sent to the Receiver");
  }).catch((e) => {
    functions.logger.log(e.toString());
  });

it's in Node JS. I am using loud Functions for this.

Musfick commented 2 years ago

@Musfick this params part is inside the apns section? Because here's mine:

await admin.messaging().send({
    token: token_o,
    notification: {},
    data: {
      imageUrl: requesterImageUrl,
      chatRoomId: chatRoomId,
      screenName: 'voiceScreen',
      voiceCall: 'voiceCall',
      callerName: requesterName,
      callsDocId: callsDocId,
      senderId: requesterId,
      callInitTime:callInitTime
    },
        android: {
          notification: {
            click_action: "android.intent.action.MAIN"
          },
        },
    apns: {
      headers: {
        "apns-push-type": "alert",
        "apns-priority": "5", // Must be `5` when `contentAvailable` is set to true.
      },
      payload: {
        aps: {
          alert: {},
          badge: 0,
          contentAvailable: true,
        },
        notification: {
          //ti `You received a call from  ${requesterName}`,
          //body: "",
          //imageUrl: requesterImageUrl,
        },
        priority: "high",
      },
    }
  }).then(value => {
    functions.logger.log("Notification for AudioCall is sent to the Receiver");
  }).catch((e) => {
    functions.logger.log(e.toString());
  });

no, it is the whole payload. I didn't use apns params

Musfick commented 2 years ago

contentAvailable: true, /// is important.You are using this inside the apns, use this inside root. like

{
          "content_available": true,
          "data": {},
          "to": "eyLsdfd_____"
}
waqadArshad commented 2 years ago

contentAvailable: true, /// is important.You are using this inside the apns, use this inside root. like

{
          "content_available": true,
          "data": {},
          "to": "eyLsdfd_____"
}

@Musfick using

contentAvailable: true,

in root throws following error in the Firebase Functions log:

Error: Invalid JSON payload received. Unknown name "contentAvailable" at 'message': Cannot find field.

image

zionnite commented 2 years ago

@waqadArshad did it work for you?

waqadArshad commented 2 years ago

@waqadArshad did it work for you?

@zionnite what? contentAvailable: true? No it didn't. Threw an error as shown in the last message.

and sorry for the delayed response. I'm just so worried about this issue. have been trying to debug this and it isn't working out.

Musfick commented 2 years ago

@waqadArshad did it work for you?

@zionnite what? contentAvailable: true? No it didn't. Threw an error as shown in the last message.

and sorry for the delayed response. I'm just so worried about this issue. have been trying to debug this and it isn't working out.

content_available not contentAvailable

waqadArshad commented 2 years ago

@zionnite well, it depends upon how you are using it. As far as I know, if you are using it in an HTTP request, it is content_available but if you are using it in a cloud function written in Node JS, you would use contentAvailable.

Musfick commented 2 years ago

no its content_available in every where

upendrarv commented 1 year ago

I have done it by only using FCM and some ios configuration. FCM is working fine in my case for receive calls and everything. Also i am using agora for video and audio call.

@Musfick : what iOS configuration changes that you have done to make it work?