flutter_callkit_incoming 3.0.0

SDKflutter
Platformandroidios

Flutter Callkit Incoming to show callkit screen in your Flutter app.

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

⭐ Features

  • Show an incoming call
  • Start an outgoing call
  • Custom UI Android/Callkit for iOS
  • Example using Pushkit/VoIP for iOS

⚠️ iOS: ONLY WORKING ON REAL DEVICE

Please make sure setup/using PUSHKIT FOR VOIP

Note: Please do not use on simulator (Callkit framework not working on simulator)

🚀 Installation

1. Install Packages

For version >= v2.5.0, please make sure install and use Java SDK version >= 17 (Android)

Run this command:

flutter pub add flutter_callkit_incoming

Or add to pubspec.yaml:

dependencies:
  flutter_callkit_incoming: ^latest

2. 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"><!-- add this -->
        ...
    </application>
</manifest>

Proguard Rules: 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>

3. Usage

Import

import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';

Show 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',
  ),
  callingNotification: const NotificationParams(
    showNotification: true,
    isShowCallback: true,
    subtitle: 'Calling...',
    callbackText: 'Hang Up',
  ),
  duration: 30000,
  extra: <String, dynamic>{'userId': '1a2b3c4d'},
  headers: <String, dynamic>{'apiKey': 'Abc@123!', 'platform': 'flutter'},
  android: const AndroidParams(
    isCustomNotification: true,
    isShowLogo: false,
    logoUrl: 'https://i.pravatar.cc/100',
    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: For Firebase Message: @pragma('vm:entry-point')
https://github.com/firebase/flutterfire/blob/master/docs/cloud-messaging/receive.md#apple-platforms-and-android

Request Notification Permission (Android 13+/iOS)

For Android 13+, please requestNotificationPermission or requestPermission of firebase_messaging before showCallkitIncoming:

await FlutterCallkitIncoming.requestNotificationPermission({
  "title": "Notification permission",
  "rationaleMessagePermission": "Notification permission is required, to show notification.",
  "postNotificationMessageRequired": "Notification permission is required, Please allow notification permission from setting."
});

Request Full Intent Permission (Android 14+)

For Android 14+, please use canUseFullScreenIntent and requestFullIntentPermission:

// Check if can use full screen intent
await FlutterCallkitIncoming.canUseFullScreenIntent();

// Request full intent permission
await FlutterCallkitIncoming.requestFullIntentPermission();

Show Missed Call Notification

this._currentUuid = _uuid.v4();
CallKitParams params = CallKitParams(
  id: _currentUuid,
  nameCaller: 'Hien Nguyen',
  handle: '0123456789',
  type: 1,
  missedCallNotification: const NotificationParams(
    showNotification: true,
    isShowCallback: true,
    subtitle: 'Missed call',
    callbackText: 'Call back',
  ),
  android: const AndroidParams(
    isCustomNotification: true,
    isShowCallID: true,
  ),
  extra: <String, dynamic>{'userId': '1a2b3c4d'},
);
await FlutterCallkitIncoming.showMissCallNotification(params);

Hide Call Notification (Android)

CallKitParams params = CallKitParams(
  id: _currentUuid,
);
await FlutterCallkitIncoming.hideCallkitIncoming(params);

Start 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'),
  callingNotification: const NotificationParams(
    showNotification: true,
    isShowCallback: true,
    subtitle: 'Calling...',
    callbackText: 'Hang Up',
  ),
  android: const AndroidParams(
    isCustomNotification: true,
    isShowCallID: true,
  )
);
await FlutterCallkitIncoming.startCall(params);

End Call

// End specific call
await FlutterCallkitIncoming.endCall(this._currentUuid);

// End all calls
await FlutterCallkitIncoming.endAllCalls();

Get Active Calls

iOS: returns active calls from Callkit (only id), Android: only returns last call

await FlutterCallkitIncoming.activeCalls();

Output:

[{"id": "8BAA2B26-47AD-42C1-9197-1D75F662DF78", ...}]

Set Call Connected

Used to determine Incoming Call or Outgoing Call status in phone book(reset/start timer):

await FlutterCallkitIncoming.setCallConnected(this._currentUuid);

Note: After the call is ACCEPT or startCall, please call this function. Normally it should be called when WebRTC/P2P is established.

Get Device Push Token VoIP

iOS: returns deviceToken, Android: returns none

await FlutterCallkitIncoming.getDevicePushTokenVoIP();

Output:

d6a77ca80c5f09f87f353cdd328ec8d7d34e92eb108d046c91906f27f54949cd

Important: 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: 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 (Android):

FlutterCallkitIncomingPlugin.getInstance().showIncomingNotification(...)

Alternative Swift approach:

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:

Swift:

SwiftFlutterCallkitIncomingPlugin.sharedInstance?.sendEventCustom(body: ["customKey": "customValue"])

Kotlin:

FlutterCallkitIncomingPlugin.getInstance().sendEventCustom(body: Map<String, Any>)

Call API when Accept/Decline/End/Timeout

Setup for Missed call notification(iOS)

AppDelegate.swift:

@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

        //Add for Missed call notification
        if #available(iOS 10.0, *) {
          UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
        }
        
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // Add for Missed call notification(show notification when foreground)
    override func userNotificationCenter(_ center: UNUserNotificationCenter,
                                willPresent notification: UNNotification,
                                withCompletionHandler completionHandler:
                                   @escaping (UNNotificationPresentationOptions) -> Void) {
        
        CallkitNotificationManager.shared.userNotificationCenter(center, willPresent: notification, withCompletionHandler: completionHandler)
    }
    
    // Add for Missed call notification(action when click callback in missed notification)
    override func userNotificationCenter(_ center: UNUserNotificationCenter,
                                         didReceive response: UNNotificationResponse,
                                         withCompletionHandler completionHandler: @escaping () -> Void) {
        if response.actionIdentifier == CallkitNotificationManager.CALLBACK_ACTION {
            let data = response.notification.request.content.userInfo as? [String: Any]
            SwiftFlutterCallkitIncomingPlugin.sharedInstance?.sendCallbackEvent(data)
        }
        completionHandler()
    }

    // 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
    }
}

Please check full example: Example

MainActivity.kt:

class MainActivity: FlutterActivity(){

    private var callkitEventCallback = object: CallkitEventCallback{
        override fun onCallEvent(event: CallkitEventCallback.CallEvent, callData: Bundle) {
            when (event) {
                CallkitEventCallback.CallEvent.ACCEPT -> {
                    // Do something with answer
                }
                CallkitEventCallback.CallEvent.DECLINE -> {
                    // Do something with decline
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        FlutterCallkitIncomingPlugin.registerEventCallback(callkitEventCallback)
    }

    override fun onDestroy() {
        FlutterCallkitIncomingPlugin.unregisterEventCallback(callkitEventCallback)
        super.onDestroy()
    }


}

Please check full example: Example

📋 Properties

Main Properties

PropertyDescriptionDefault
idUUID 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 UUIDRequired
nameCallerCaller's nameNone
appNameApp's name. Used for display inside Callkit (iOS).App Name, Deprecated for iOS > 14, default using App name
avatarAvatar's URL used for display for Android. /android/src/main/res/drawable-xxxhdpi/ic_default_avatar.pngNone
handlePhone number/Email/AnyNone
type0 - Audio Call, 1 - Video Call0
durationIncoming call/Outgoing call display time (second). If the time is over, the call will be missed30000
textAcceptText Accept used in AndroidAccept
textDeclineText Decline used in AndroidDecline
extraAny data added to the event when received{}
headersAny data for custom header avatar/background image{}
missedCallNotificationAndroid data needed to customize Missed Call NotificationBelow
callingNotificationAndroid data needed to customize Calling NotificationBelow
androidAndroid data needed to customize UIBelow
iosiOS data neededBelow

Missed Call Notification

PropertyDescriptionDefault
subtitleText Missed Call used in Android/iOS (show in missed call notification)Missed Call
callbackTextText Call back used in Android/iOS (show in missed call notification action)Call back
showNotificationShow missed call notification when timeouttrue
isShowCallbackShow callback action from missed call notificationtrue

Calling Notification

PropertyDescriptionDefault
subtitleText used in Android (show in calling notification)Calling...
callbackTextText used in Android (show in calling notification action)Hang Up
showNotificationShow calling notification when start call/accept calltrue
isShowCallbackShow hang up action from calling notificationtrue

Android

PropertyDescriptionDefault
isCustomNotificationUsing custom notificationsfalse
isCustomSmallExNotificationUsing custom notification small on some devices clipped out in Androidfalse
isShowLogoShow logo app inside full screen. /android/src/main/res/drawable-xxxhdpi/ic_logo.pngfalse
logoUrlLogo app inside full screen. Example: http://... https://... or "assets/abc.png"None
ringtonePathFile name of a ringtone ex: ringtone_default. Put file into /android/app/src/main/res/raw/ringtone_default.mp3system_ringtone_default
using ringtone default of the phone
backgroundColorIncoming call screen background color#0955fa
backgroundUrlUsing image background for Incoming call screen. Example: http://... https://... or "assets/abc.png"None
actionColorColor used in button/text on notification#4CAF50
textColorColor used for the text in full screen notification#ffffff
incomingCallNotificationChannelNameNotification channel name of incoming callIncoming call
missedCallNotificationChannelNameNotification channel name of missed callMissed call
isShowCallIDShow call id app inside full screen/notificationfalse
isShowFullLockedScreenShow full screen on Locked Screen (please make sure call requestFullIntentPermission for Android 14+)true

iOS

PropertyDescriptionDefault
iconNameApp's Icon. Used for display inside Callkit (iOS)CallKitLogo
using from Images.xcassets/CallKitLogo
handleTypeType handle call generic, number, email, Recommended to use generic for more reasonable callkit displaygeneric
supportsVideotrue
maximumCallGroups2
maximumCallsPerCallGroup1
audioSessionModeNone, gameChat, measurement, moviePlayback, spokenAudio, videoChat, videoRecording, voiceChat, voicePrompt
audioSessionActivetrue
audioSessionPreferredSampleRate44100.0
audioSessionPreferredIOBufferDuration0.005
supportsDTMFtrue
supportsHoldingtrue
supportsGroupingtrue
supportsUngroupingtrue
ringtonePathAdd file to root project xcode /ios/Runner/Ringtone.caf and Copy Bundle Resources (Build Phases)Ringtone.caf
system_ringtone_default
using ringtone default of the phone

📁 Source Code

Please checkout repo GitHub:

📱 Pushkit - Received VoIP and Wake App from Terminated State (iOS Only)

Please check PUSHKIT.md for setup Pushkit for iOS.

📋 Todo

  • Run background
  • Simplify the setup process
  • Custom notification for iOS (Missing notification)
  • Keep notification when calling

🎯 Demo

Demo Illustration

Images

iOS (Lockscreen)iOS (Full Screen)iOS (Alert)
iOS Lockscreen iOS Full Screen iOS Alert
Android (Lockscreen) - AudioAndroid (Alert) - AudioAndroid (Lockscreen) - Video
Android Lockscreen Audio Android Alert Audio Android Lockscreen Video
Android (Alert) - VideoisCustomNotification: false
Android Alert Video Custom Notification False