|
5 | 5 | * iOS CallKit and Android ConnectionService for Flutter |
6 | 6 | * Support FCM and PushKit |
7 | 7 |
|
8 | | -## push payload |
| 8 | +> Keep in mind Callkit is banned in China, so if you want your app in the chinese AppStore consider include a basic alternative for notifying calls (ex. FCM notifications with sound). |
| 9 | +
|
| 10 | +`* P-C-M means -> presenter / controller / manager` |
| 11 | + |
| 12 | +## Introduction |
| 13 | + |
| 14 | +Callkeep acts as an intermediate between your call system (RTC, VOIP...) and the user, offering a native calling interface for handling your app calls. |
| 15 | + |
| 16 | +This allows you (for example) to answer calls when your device is locked even if your app is terminated. |
| 17 | + |
| 18 | + |
| 19 | +## Initial setup |
| 20 | + |
| 21 | +Basic configuration. In Android a popup is displayed before starting requesting some permissions to work properly. |
| 22 | + |
| 23 | +```dart |
| 24 | +final callSetup = <String, dynamic>{ |
| 25 | + 'ios': { |
| 26 | + 'appName': 'CallKeepDemo', |
| 27 | + }, |
| 28 | + 'android': { |
| 29 | + 'alertTitle': 'Permissions required', |
| 30 | + 'alertDescription': |
| 31 | + 'This application needs to access your phone accounts', |
| 32 | + 'cancelButton': 'Cancel', |
| 33 | + 'okButton': 'ok', |
| 34 | + }, |
| 35 | +}; |
| 36 | +
|
| 37 | +callKeep.setup(callSetup); |
| 38 | +``` |
| 39 | + |
| 40 | +This configuration should be defined when your application wakes up, but keep in mind this alert will appear if you aren't granting the needed permissions yet. |
| 41 | + |
| 42 | +A clean alternative is to control by yourself the required permissions when your application wakes up, and only invoke the `setup()` method if those permissions are granted. |
| 43 | + |
| 44 | +## Events |
| 45 | + |
| 46 | +Callkeep offers some events to handle native actions during a call. |
| 47 | + |
| 48 | +These events are quite crucial because they act as an intermediate between the native calling UI and your call P-C-M. |
| 49 | + |
| 50 | +What does it mean? |
| 51 | + |
| 52 | +Assuming your application already implements some calling system (RTC, Voip, or whatever) with its own calling UI, you are using some basic controls: |
| 53 | + |
| 54 | +<img width="40%" vspace="10" src="https://raw.githubusercontent.com/efraespada/callkeep/master/images/sample.png"></p> |
| 55 | + |
| 56 | +> before implementing `callkeep` |
| 57 | +
|
| 58 | +- Hang up -> `presenter.hangUp()` |
| 59 | +- Microphone switcher -> `presenter.microSwitch()` |
| 60 | + |
| 61 | +> after implementing `callkeep` |
| 62 | +
|
| 63 | +- Hang up -> `callkeep.endCall(call_uuid)` |
| 64 | +- Microphone switcher -> `callKeep.setMutedCall(uuid, true / false)` |
| 65 | + |
| 66 | +Then you handle the action: |
| 67 | + |
| 68 | +```dart |
| 69 | +Function(CallKeepPerformAnswerCallAction) answerAction = (event) async { |
| 70 | + print('CallKeepPerformAnswerCallAction ${event.callUUID}'); |
| 71 | + // notify to your call P-C-M the answer action |
| 72 | +}; |
| 73 | +
|
| 74 | +Function(CallKeepPerformEndCallAction) endAction = (event) async { |
| 75 | + print('CallKeepPerformEndCallAction ${event.callUUID}'); |
| 76 | + // notify to your call P-C-M the end action |
| 77 | +}; |
| 78 | +
|
| 79 | +Function(CallKeepDidPerformSetMutedCallAction) setMuted = (event) async { |
| 80 | + print('CallKeepDidPerformSetMutedCallAction ${event.callUUID}'); |
| 81 | + // notify to your call P-C-M the muted switch action |
| 82 | +}; |
| 83 | +
|
| 84 | +Function(CallKeepDidToggleHoldAction) onHold = (event) async { |
| 85 | + print('CallKeepDidToggleHoldAction ${event.callUUID}'); |
| 86 | + // notify to your call P-C-M the hold switch action |
| 87 | +}; |
| 88 | +``` |
| 89 | + |
| 90 | +```dart |
| 91 | +callKeep.on(CallKeepDidToggleHoldAction(), onHold); |
| 92 | +callKeep.on(CallKeepPerformAnswerCallAction(), answerAction); |
| 93 | +callKeep.on(CallKeepPerformEndCallAction(), endAction); |
| 94 | +callKeep.on(CallKeepDidPerformSetMutedCallAction(), setMuted); |
| 95 | +``` |
| 96 | + |
| 97 | +## Display incoming calls in foreground, background or terminate state |
| 98 | + |
| 99 | +The incoming call concept we are looking for is firing an incoming call action when "something" is received in our app. |
| 100 | + |
| 101 | +I've tested this concept with FCM and it works pretty fine. |
| 102 | + |
| 103 | +```dart |
| 104 | +final FlutterCallkeep _callKeep = FlutterCallkeep(); |
| 105 | +bool _callKeepStarted = false; |
| 106 | +
|
| 107 | +Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async { |
| 108 | + await Firebase.initializeApp(); |
| 109 | + if (!_callKeepStarted) { |
| 110 | + try { |
| 111 | + await _callKeep.setup(callSetup); |
| 112 | + _callKeepStarted = true; |
| 113 | + } catch (e) { |
| 114 | + print(e); |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + // then process your remote message looking for some call uuid |
| 119 | + // and display any incoming call |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +Displaying incoming calls is really simple if you are receiving FCM messages (or whatever). This example shows how to show and close any incoming call: |
| 124 | + |
| 125 | +> Notice that getting data from the payload can be done as you want, this is an example. |
| 126 | +
|
| 127 | +A payload data example: |
9 | 128 |
|
10 | 129 | ```json |
11 | 130 | { |
12 | 131 | "uuid": "xxxxx-xxxxx-xxxxx-xxxxx", |
13 | | - "caller_id": "+8618612345678", |
14 | | - "caller_name": "hello", |
| 132 | + "caller_id": "+0123456789", |
| 133 | + "caller_name": "Draco", |
15 | 134 | "caller_id_type": "number", |
16 | | - "has_video": false, |
| 135 | + "has_video": "false" |
17 | 136 | } |
18 | 137 | ``` |
19 | 138 |
|
| 139 | +A `RemoteMessage` extension for getting data: |
| 140 | + |
| 141 | +```dart |
| 142 | +import 'dart:convert'; |
| 143 | +
|
| 144 | +import 'package:firebase_messaging/firebase_messaging.dart'; |
| 145 | +
|
| 146 | +extension RemoteMessageExt on RemoteMessage { |
| 147 | + Map<String, dynamic> getContent() { |
| 148 | + return jsonDecode(this.data["content"]); |
| 149 | + } |
| 150 | +
|
| 151 | + Map<String, dynamic> payload() { |
| 152 | + return getContent()["payload"]; |
| 153 | + } |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +Methods to show and close incoming calls: |
| 158 | + |
| 159 | +```dart |
| 160 | +Future<void> showIncomingCall( |
| 161 | + BuildContext context, |
| 162 | + RemoteMessage remoteMessage, |
| 163 | + FlutterCallkeep callKeep, |
| 164 | +) async { |
| 165 | + var callerIdFrom = remoteMessage.payload()["caller_id"] as String; |
| 166 | + var callerName = remoteMessage.payload()["caller_name"] as String; |
| 167 | + var uuid = remoteMessage.payload()["uuid"] as String; |
| 168 | + var hasVideo = remoteMessage.payload()["has_video"] == "true"; |
| 169 | + |
| 170 | + callKeep.on(CallKeepDidToggleHoldAction(), onHold); |
| 171 | + callKeep.on(CallKeepPerformAnswerCallAction(), answerAction); |
| 172 | + callKeep.on(CallKeepPerformEndCallAction(), endAction); |
| 173 | + callKeep.on(CallKeepDidPerformSetMutedCallAction(), setMuted); |
| 174 | +
|
| 175 | + print('backgroundMessage: displayIncomingCall ($uuid)'); |
| 176 | +
|
| 177 | + bool hasPhoneAccount = await callKeep.hasPhoneAccount(); |
| 178 | + if (!hasPhoneAccount) { |
| 179 | + hasPhoneAccount = await callKeep.hasDefaultPhoneAccount(context, callSetup["android"]); |
| 180 | + } |
| 181 | +
|
| 182 | + if (!hasPhoneAccount) { |
| 183 | + return; |
| 184 | + } |
| 185 | +
|
| 186 | + await callKeep.displayIncomingCall(uuid, callerIdFrom, localizedCallerName: callerName, hasVideo: hasVideo); |
| 187 | + callKeep.backToForeground(); |
| 188 | +} |
| 189 | +
|
| 190 | +Future<void> closeIncomingCall( |
| 191 | + RemoteMessage remoteMessage, |
| 192 | + FlutterCallkeep callKeep, |
| 193 | +) async { |
| 194 | + var uuid = remoteMessage.payload()[MessageManager.CALLER_UUID] as String; |
| 195 | + print('backgroundMessage: closeIncomingCall ($uuid)'); |
| 196 | + bool hasPhoneAccount = await callKeep.hasPhoneAccount(); |
| 197 | + if (!hasPhoneAccount) { |
| 198 | + return; |
| 199 | + } |
| 200 | + await callKeep.endAllCalls(); |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +### FAQ |
| 205 | + |
| 206 | +> I don't receive the incoming call |
| 207 | +
|
| 208 | +Receiving incoming calls depends on FCM push messages (or the system you use) for handling the call information and displaying it. |
| 209 | +Remember FCM push messages not always works due to data-only messages are classified as "low priority". Devices can throttle and ignore these messages if your application is in the background, terminated, or a variety of other conditions such as low battery or currently high CPU usage. To help improve delivery, you can bump the priority of messages. Note; this does still not guarantee delivery. More info [here](https://firebase.flutter.dev/docs/messaging/usage/#low-priority-messages) |
| 210 | + |
| 211 | +> How can I manage the call if the app is terminated and the device is locked? |
| 212 | +
|
| 213 | +Even in this scenario, the `backToForeground()` method will open the app and your call P-C-M will be able to work. |
| 214 | + |
| 215 | + |
20 | 216 | ## push test tool |
21 | 217 |
|
22 | 218 | Please refer to the [Push Toolkit](/tools/) to test callkeep offline push. |
0 commit comments