hiennguyen92 / flutter_callkit_incoming

Flutter Callkit Incoming
https://pub.dev/packages/flutter_callkit_incoming
MIT License
180 stars 312 forks source link
android callkit dart flutter incoming ios

Flutter Callkit Incoming

A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Callkit for iOS).

pub package pub points GitHub stars GitHub forks GitHub license Build Status

Sponsors

Our top sponsors are shown below!


Try the Flutter Video Tutorial šŸ“¹

Buy Me A Coffee

:star: Features

iOS: ONLY WORKING ON REAL DEVICE, not on simulator(Callkit framework not working on simulator)


šŸš€  Installation

  1. Install Packages

    • Run this command:
      flutter pub add flutter_callkit_incoming
    • Add pubspec.yaml:

        dependencies:
          flutter_callkit_incoming: any
      1. Configure Project

        • Android
        • AndroidManifest.xml

          <manifest...>
           ...
           <!--
               Using for load image from internet
           -->
           <uses-permission android:name="android.permission.INTERNET"/>
          
          <application ...>
             <activity ...
                android:name=".MainActivity"
                android:launchMode="singleInstance">
              ...
          ...
          
          </manifest>

          The following rule needs to be added in the proguard-rules.pro to avoid obfuscated keys.

          -keep class com.hiennv.flutter_callkit_incoming.** { *; }
    • iOS
      • Info.plist
        <key>UIBackgroundModes</key>
        <array>
        <string>voip</string>
        <string>remote-notification</string>
        <string>processing</string> //you can add this if needed
        </array>
  2. Usage

    • Import

      import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
    • Received an incoming call

      this._currentUuid = _uuid.v4();
      CallKitParams callKitParams = CallKitParams(
      id: _currentUuid,
      nameCaller: 'Hien Nguyen',
      appName: 'Callkit',
      avatar: 'https://i.pravatar.cc/100',
      handle: '0123456789',
      type: 0,
      textAccept: 'Accept',
      textDecline: 'Decline',
      missedCallNotification: NotificationParams(
          showNotification: true,
          isShowCallback: true,
          subtitle: 'Missed call',
          callbackText: 'Call back',
      ),
      duration: 30000,
      extra: <String, dynamic>{'userId': '1a2b3c4d'},
      headers: <String, dynamic>{'apiKey': 'Abc@123!', 'platform': 'flutter'},
      android: const AndroidParams(
          isCustomNotification: true,
          isShowLogo: false,
          ringtonePath: 'system_ringtone_default',
          backgroundColor: '#0955fa',
          backgroundUrl: 'https://i.pravatar.cc/500',
          actionColor: '#4CAF50',
          textColor: '#ffffff',
          incomingCallNotificationChannelName: "Incoming Call",
          missedCallNotificationChannelName: "Missed Call",
          isShowCallID: false
      ),
      ios: 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',
      ),
      );
      await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);

      Note: Firebase Message: @pragma('vm:entry-point')
      https://github.com/firebase/flutterfire/blob/master/docs/cloud-messaging/receive.md#apple-platforms-and-android

    • request permission for post Notification Android 13+ For Android 13+, please requestNotificationPermission or requestPermission of firebase_messaging before showCallkitIncoming

      await FlutterCallkitIncoming.requestNotificationPermission({
      "rationaleMessagePermission": "Notification permission is required, to show notification.",
      "postNotificationMessageRequired": "Notification permission is required, Please allow notification permission from setting."
      });
    • request permission for full intent Notification/full screen locked screen Android 14+ For Android 14+, please requestFullIntentPermission

      await FlutterCallkitIncoming.requestFullIntentPermission();
    • Show miss call notification

      this._currentUuid = _uuid.v4();
      CallKitParams params = CallKitParams(
      id: _currentUuid,
      nameCaller: 'Hien Nguyen',
      handle: '0123456789',
      type: 1,
      textMissedCall: 'Missed call',
      textCallback: 'Call back',
      extra: <String, dynamic>{'userId': '1a2b3c4d'},
      );
      await FlutterCallkitIncoming.showMissCallNotification(params);
    • Hide notification call for Android

      CallKitParams params = CallKitParams(
      id: _currentUuid,
      );
      await FlutterCallkitIncoming.hideCallkitIncoming(params);
    • Started an outgoing call

      this._currentUuid = _uuid.v4();
      CallKitParams params = CallKitParams(
      id: this._currentUuid,
      nameCaller: 'Hien Nguyen',
      handle: '0123456789',
      type: 1,
      extra: <String, dynamic>{'userId': '1a2b3c4d'},
      ios: IOSParams(handleType: 'generic')
      );
      await FlutterCallkitIncoming.startCall(params);
    • Ended an incoming/outgoing call

      await FlutterCallkitIncoming.endCall(this._currentUuid);
    • Ended all calls

      await FlutterCallkitIncoming.endAllCalls();
    • Get active calls. iOS: return active calls from Callkit(only id), Android: only return last call

      await FlutterCallkitIncoming.activeCalls();

      Output

      [{"id": "8BAA2B26-47AD-42C1-9197-1D75F662DF78", ...}]
    • Set status call connected (only for iOS - used to determine Incoming Call or Outgoing Call status in phone book)

      await FlutterCallkitIncoming.setCallConnected(this._currentUuid);

      After the call is ACCEPT or startCall please call this func. normally it should be called when webrtc/p2p.... is established.

    • Get device push token VoIP. iOS: return deviceToken, Android: Empty

      await FlutterCallkitIncoming.getDevicePushTokenVoIP();

    Output

    <device token>
    
    //Example
    d6a77ca80c5f09f87f353cdd328ec8d7d34e92eb108d046c91906f27f54949cd
    

    Make sure using SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken) inside AppDelegate.swift (Example)

    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
        print(credentials.token)
        let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
        //Save deviceToken to your server
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
    }
    
    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
        print("didInvalidatePushTokenFor")
        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
    }
    • Listen events
      FlutterCallkitIncoming.onEvent.listen((CallEvent event) {
      switch (event!.event) {
        case Event.actionCallIncoming:
          // TODO: received an incoming call
          break;
        case Event.actionCallStart:
          // TODO: started an outgoing call
          // TODO: show screen calling in Flutter
          break;
        case Event.actionCallAccept:
          // TODO: accepted an incoming call
          // TODO: show screen calling in Flutter
          break;
        case Event.actionCallDecline:
          // TODO: declined an incoming call
          break;
        case Event.actionCallEnded:
          // TODO: ended an incoming/outgoing call
          break;
        case Event.actionCallTimeout:
          // TODO: missed an incoming call
          break;
        case Event.actionCallCallback:
          // TODO: only Android - click action `Call back` from missed call notification
          break;
        case Event.actionCallToggleHold:
          // TODO: only iOS
          break;
        case Event.actionCallToggleMute:
          // TODO: only iOS
          break;
        case Event.actionCallToggleDmtf:
          // TODO: only iOS
          break;
        case Event.actionCallToggleGroup:
          // TODO: only iOS
          break;
        case Event.actionCallToggleAudioSession:
          // TODO: only iOS
          break;
        case Event.actionDidUpdateDevicePushTokenVoip:
          // TODO: only iOS
          break;
        case Event.actionCallCustom:
          // TODO: for custom action
          break;
      }
      });
    • Call from Native (iOS/Android)
      //Swift iOS
      var info = [String: Any?]()
      info["id"] = "44d915e1-5ff4-4bed-bf13-c423048ec97a"
      info["nameCaller"] = "Hien Nguyen"
      info["handle"] = "0123456789"
      info["type"] = 1
      //... set more data
      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(flutter_callkit_incoming.Data(args: info), fromPushKit: true)
    
      //please make sure call `completion()` at the end of the pushRegistry(......, completion: @escaping () -> Void)
      // or `DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { completion() }`
      // if you don't call completion() in pushRegistry(......, completion: @escaping () -> Void), there may be app crash by system when receiving voIP
        //Kotlin/Java Android
        FlutterCallkitIncomingPlugin.getInstance().showIncomingNotification(...)


      //OR
      let data = flutter_callkit_incoming.Data(id: "44d915e1-5ff4-4bed-bf13-c423048ec97a", nameCaller: "Hien Nguyen", handle: "0123456789", type: 0)
      data.nameCaller = "Johnny"
      data.extra = ["user": "abc@123", "platform": "ios"]
      //... set more data
      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)


      //Objective-C
      #if __has_include(<flutter_callkit_incoming/flutter_callkit_incoming-Swift.h>)
      #import <flutter_callkit_incoming/flutter_callkit_incoming-Swift.h>
      #else
      #import "flutter_callkit_incoming-Swift.h"
      #endif
    
      Data * data = [[Data alloc]initWithId:@"44d915e1-5ff4-4bed-bf13-c423048ec97a" nameCaller:@"Hien Nguyen" handle:@"0123456789" type:1];
      [data setNameCaller:@"Johnny"];
      [data setExtra:@{ @"userId" : @"HelloXXXX", @"key2" : @"value2"}];
      //... set more data
      [SwiftFlutterCallkitIncomingPlugin.sharedInstance showCallkitIncoming:data fromPushKit:YES];


      //send custom event from native
      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.sendEventCustom(body: ["customKey": "customValue"])
    
        //Kotlin/Java Android
        FlutterCallkitIncomingPlugin.getInstance().sendEventCustom(body: Map<String, Any>)
    • 3.1 Call API when accept/decline/end/timeout
      
      //Appdelegate
      ...
      @UIApplicationMain
      @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
      ...

    override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self)

    //Setup VOIP
    let mainQueue = DispatchQueue.main
    let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
    voipRegistry.delegate = self
    voipRegistry.desiredPushTypes = [PKPushType.voIP]
    
    //Use if using WebRTC
    //RTCAudioSession.sharedInstance().useManualAudio = true
    //RTCAudioSession.sharedInstance().isAudioEnabled = false
    
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)

    }

    // Func Call api for Accept func onAccept( call: Call, action: CXAnswerCallAction) { let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any] print("LOG: onAccept") self.performRequest(parameters: json) { result in switch result { case .success(let data): print("Received data: (data)") //Make sure call action.fulfill() when you are done(connected WebRTC - Start counting seconds) action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }

    }

    // Func Call API for Decline func onDecline( call: Call, action: CXEndCallAction) { let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any] print("LOG: onDecline") self.performRequest(parameters: json) { result in switch result { case .success(let data): print("Received data: (data)") //Make sure call action.fulfill() when you are done action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }

    }

    // Func Call API for End func onEnd( call: Call, action: CXEndCallAction) { let json = ["action": "END", "data": call.data.toJSON()] as [String: Any] print("LOG: onEnd") self.performRequest(parameters: json) { result in switch result { case .success(let data): print("Received data: (data)") //Make sure call action.fulfill() when you are done action.fulfill()

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }

    }

    func onTimeOut(_ call: Call) { let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any] print("LOG: onTimeOut") self.performRequest(parameters: json) { result in switch result { case .success(let data): print("Received data: (data)")

        case .failure(let error):
            print("Error: \(error.localizedDescription)")
        }
    }

    }

    func didActivateAudioSession(_ audioSession: AVAudioSession) { //Use if using WebRTC //RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession) //RTCAudioSession.sharedInstance().isAudioEnabled = true }

    func didDeactivateAudioSession(_ audioSession: AVAudioSession) { //Use if using WebRTC //RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession) //RTCAudioSession.sharedInstance().isAudioEnabled = false } ...

    
    <a href='https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/ios/Runner/AppDelegate.swift'>Please check full: Example</a>
  3. Properties

    Prop Description Default
    id UUID identifier for each call. UUID should be unique for every call and when the call is ended, the same UUID for that call to be used. suggest using uuid. ACCEPT ONLY UUID Required
    nameCaller Caller's name. None
    appName App's name. using for display inside Callkit(iOS). App Name, Deprecated for iOS > 14, default using App name
    avatar Avatar's URL used for display for Android. /android/src/main/res/drawable-xxxhdpi/ic_default_avatar.png None
    handle Phone number/Email/Any. None
    type 0 - Audio Call, 1 - Video Call 0
    duration Incoming call/Outgoing call display time (second). If the time is over, the call will be missed. 30000
    textAccept Text Accept used in Android Accept
    textDecline Text Decline used in Android Decline
    extra Any data added to the event when received. {}
    headers Any data for custom header avatar/background image. {}
    missedCallNotification Android data needed to customize Miss Call Notification. Below
    android Android data needed to customize UI. Below
    ios iOS data needed. Below


  1. Source code

    please checkout repo github
    https://github.com/hiennguyen92/flutter_callkit_incoming


  2. Pushkit - Received VoIP and Wake app from Terminated State (only for IOS)


  3. Todo

    • Run background
    • Simplify the setup process


:bulb: Demo

  1. Demo Illustration:
  2. Image
    iOS(Lockscreen) iOS(full screen) iOS(Alert)
    Android(Lockscreen) - Audio Android(Alert) - Audio Android(Lockscreen) - Video
    Android(Alert) - Video isCustomNotification: false