Skip to content

Commit

Permalink
Extracting UI responsibility, named parameters, android dismissal fix (
Browse files Browse the repository at this point in the history
…#189)

Co-authored-by: Mikael Wills <mikael.wills@ideaworks.co.uk>
  • Loading branch information
mikaelwills and Mikael Wills authored Aug 24, 2024
1 parent c5ee757 commit 0377bfd
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 170 deletions.
89 changes: 64 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

[![Financial Contributors on Open Collective](https://opencollective.com/flutter-webrtc/all/badge.svg?label=financial+contributors)](https://opencollective.com/flutter-webrtc) [![pub package](https://img.shields.io/pub/v/callkeep.svg)](https://pub.dartlang.org/packages/callkeep) [![slack](https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen)](https://join.slack.com/t/flutterwebrtc/shared_invite/zt-q83o7y1s-FExGLWEvtkPKM8ku_F8cEQ)

* iOS CallKit and Android ConnectionService for Flutter
* Support FCM and PushKit
- iOS CallKit and Android ConnectionService for Flutter
- Support FCM and PushKit

> 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).
Expand Down Expand Up @@ -36,7 +36,7 @@ final callSetup = <String, dynamic>{
'channelName': 'Foreground service for my app',
'notificationTitle': 'My app is running on background',
'notificationIcon': 'mipmap/ic_notification_launcher',
},
},
},
};
Expand All @@ -53,7 +53,7 @@ Callkeep offers some events to handle native actions during a call.

These events are quite crucial because they act as an intermediate between the native calling UI and your call P-C-M.

What does it mean?
What does it mean?

Assuming your application already implements some calling system (RTC, Voip, or whatever) with its own calling UI, you are using some basic controls:

Expand All @@ -72,32 +72,37 @@ Assuming your application already implements some calling system (RTC, Voip, or
Then you handle the action:

```dart
Function(CallKeepPerformAnswerCallAction) answerAction = (event) async {
Future<void> answerCall(CallKeepPerformAnswerCallAction event) async {
print('CallKeepPerformAnswerCallAction ${event.callUUID}');
// notify to your call P-C-M the answer action
};
Function(CallKeepPerformEndCallAction) endAction = (event) async {
Future<void> endCall(CallKeepPerformEndCallAction event) async {
print('CallKeepPerformEndCallAction ${event.callUUID}');
// notify to your call P-C-M the end action
};
Function(CallKeepDidPerformSetMutedCallAction) setMuted = (event) async {
Future<void> didPerformSetMutedCallAction(CallKeepDidPerformSetMutedCallAction event) async {
print('CallKeepDidPerformSetMutedCallAction ${event.callUUID}');
// notify to your call P-C-M the muted switch action
};
Function(CallKeepDidToggleHoldAction) onHold = (event) async {
Future<void> didToggleHoldCallAction(CallKeepDidToggleHoldAction event) async {
print('CallKeepDidToggleHoldAction ${event.callUUID}');
// notify to your call P-C-M the hold switch action
};
```

```dart
callKeep.on(CallKeepDidToggleHoldAction(), onHold);
callKeep.on(CallKeepPerformAnswerCallAction(), answerAction);
callKeep.on(CallKeepPerformEndCallAction(), endAction);
callKeep.on(CallKeepDidPerformSetMutedCallAction(), setMuted);
@override
void initState() {
super.initState();
callKeep.on<CallKeepDidDisplayIncomingCall>(didDisplayIncomingCall);
callKeep.on<CallKeepPerformAnswerCallAction>(answerCall);
callKeep.on<CallKeepPerformEndCallAction>(endCall);
callKeep.on<CallKeepDidToggleHoldAction>(didToggleHoldCallAction);
}
```

## Display incoming calls in foreground, background or terminate state
Expand All @@ -120,7 +125,7 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
print(e);
}
}
// then process your remote message looking for some call uuid
// and display any incoming call
}
Expand All @@ -134,11 +139,11 @@ A payload data example:

```json
{
"uuid": "xxxxx-xxxxx-xxxxx-xxxxx",
"caller_id": "+0123456789",
"caller_name": "Draco",
"caller_id_type": "number",
"has_video": "false"
"uuid": "xxxxx-xxxxx-xxxxx-xxxxx",
"caller_id": "+0123456789",
"caller_name": "Draco",
"caller_id_type": "number",
"has_video": "false"
}
```

Expand Down Expand Up @@ -172,11 +177,11 @@ Future<void> showIncomingCall(
var callerName = remoteMessage.payload()["caller_name"] as String;
var uuid = remoteMessage.payload()["uuid"] as String;
var hasVideo = remoteMessage.payload()["has_video"] == "true";
callKeep.on(CallKeepDidToggleHoldAction(), onHold);
callKeep.on(CallKeepPerformAnswerCallAction(), answerAction);
callKeep.on(CallKeepPerformEndCallAction(), endAction);
callKeep.on(CallKeepDidPerformSetMutedCallAction(), setMuted);
callKeep.on<CallKeepDidToggleHoldAction>(onHold);
callKeep.on<CallKeepPerformAnswerCallAction>(answerAction);
callKeep.on<CallKeepPerformEndCallAction>(endAction);
callKeep.on<CallKeepDidPerformSetMutedCallAction>(setMuted);
print('backgroundMessage: displayIncomingCall ($uuid)');
Expand Down Expand Up @@ -207,6 +212,40 @@ Future<void> closeIncomingCall(
}
```

Pass in your own dialog UI for permissions alerts

````dart
showAlertDialog: () async {
final BuildContext context = navigatorKey.currentContext!;
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Permissions Required'),
content: const Text(
'This application needs to access your phone accounts'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(false),
),
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(true),
),
],
);
},
) ??
false;
},
```
### FAQ
> I don't receive the incoming call
Expand All @@ -216,9 +255,9 @@ Remember FCM push messages not always works due to data-only messages are classi
> How can I manage the call if the app is terminated and the device is locked?
Even in this scenario, the `backToForeground()` method will open the app and your call P-C-M will be able to work.

Even in this scenario, the `backToForeground()` method will open the app and your call P-C-M will be able to work.
## push test tool
Please refer to the [Push Toolkit](/tools/) to test callkeep offline push.
````
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ public void onAnswer(int videoState) {

private void onAnswered() {
initCall();
setCurrent();
sendCallRequestToActivity(ACTION_ANSWER_CALL, connectionData);
}

Expand Down
121 changes: 72 additions & 49 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,34 +57,32 @@ Future<dynamic> myBackgroundMessageHandler(RemoteMessage message) {
});
if (!_callKeepInited) {
_callKeep.setup(
null,
<String, dynamic>{
'ios': {
'appName': 'CallKeepDemo',
},
'android': {
'alertTitle': 'Permissions required',
'alertDescription':
'This application needs to access your phone accounts',
'cancelButton': 'Cancel',
'okButton': 'ok',
'foregroundService': {
'channelId': 'com.company.my',
'channelName': 'Foreground service for my app',
'notificationTitle': 'My app is running on background',
'notificationIcon':
'Path to the resource icon of the notification',
},
showAlertDialog: null,
options: <String, dynamic>{
'ios': {
'appName': 'CallKeepDemo',
},
'android': {
'additionalPermissions': [
'android.permission.CALL_PHONE',
'android.permission.READ_PHONE_NUMBERS'
],
'foregroundService': {
'channelId': 'com.example.call-kit-test',
'channelName': 'callKitTest',
'notificationTitle': 'My app is running on background',
'notificationIcon': 'Path to the resource icon of the notification',
},
},
backgroundMode: true);
},
);
_callKeepInited = true;
}

logger.d('backgroundMessage: displayIncomingCall ($callerId)');
_callKeep.displayIncomingCall(
callUUID,
callerId,
uuid: callUUID,
handle: callerId,
callerName: callerName,
hasVideo: hasVideo,
);
Expand Down Expand Up @@ -220,7 +218,8 @@ class MyAppState extends State<HomePage> {
logger
.d('[didReceiveStartCallAction] $callUUID, number: ${callData.handle}');

_callKeep.startCall(callUUID, call.number, call.number);
_callKeep.startCall(
uuid: callUUID, handle: call.number, callerName: call.number);

Timer(const Duration(seconds: 1), () {
logger.d('[setCurrentCallActive] $callUUID, number: ${callData.handle}');
Expand Down Expand Up @@ -263,14 +262,14 @@ class MyAppState extends State<HomePage> {
}

Future<void> setOnHold(String callUUID, bool held) async {
_callKeep.setOnHold(callUUID, held);
_callKeep.setOnHold(uuid: callUUID, shouldHold: held);
final String handle = calls[callUUID]?.number ?? "No Number";
logger.d('[setOnHold: $held] $callUUID, number: $handle');
setCallHeld(callUUID, held);
}

Future<void> setMutedCall(String callUUID, bool muted) async {
_callKeep.setMutedCall(callUUID, muted);
_callKeep.setMutedCall(uuid: callUUID, shouldMute: muted);
final String handle = calls[callUUID]?.number ?? "No Number";
logger.d('[setMutedCall: $muted] $callUUID, number: $handle');
setCallMuted(callUUID, muted);
Expand All @@ -280,9 +279,11 @@ class MyAppState extends State<HomePage> {
final String number = calls[callUUID]?.number ?? "No Number";
// Workaround because Android doesn't display well displayName, se we have to switch ...
if (isIOS) {
_callKeep.updateDisplay(callUUID, callerName: 'New Name', handle: number);
_callKeep.updateDisplay(
uuid: callUUID, callerName: 'New Name', handle: number);
} else {
_callKeep.updateDisplay(callUUID, callerName: number, handle: 'New Name');
_callKeep.updateDisplay(
uuid: callUUID, callerName: number, handle: 'New Name');
}

logger.d('[updateDisplay: $number] $callUUID');
Expand All @@ -302,7 +303,7 @@ class MyAppState extends State<HomePage> {
logger.d('Display incoming call now');
final bool hasPhoneAccount = await _callKeep.hasPhoneAccount();
if (!hasPhoneAccount) {
await _callKeep.hasDefaultPhoneAccount(context, <String, dynamic>{
await _callKeep.hasDefaultPhoneAccount(<String, dynamic>{
'alertTitle': 'Permissions required',
'alertDescription':
'This application needs to access your phone accounts',
Expand All @@ -318,8 +319,8 @@ class MyAppState extends State<HomePage> {
}

logger.d('[displayIncomingCall] $callUUID number: $number');
_callKeep.displayIncomingCall(callUUID, number,
handleType: 'number', hasVideo: false);
_callKeep.displayIncomingCall(
uuid: callUUID, handle: number, handleType: 'number', hasVideo: false);
}

void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) {
Expand Down Expand Up @@ -347,29 +348,51 @@ class MyAppState extends State<HomePage> {
_callKeep.on<CallKeepDidPerformDTMFAction>(didPerformDTMFAction);
_callKeep.on<CallKeepDidReceiveStartCallAction>(didReceiveStartCallAction);
_callKeep.on<CallKeepDidToggleHoldAction>(didToggleHoldCallAction);
_callKeep.on<CallKeepDidPerformSetMutedCallAction>(didPerformSetMutedCallAction);
_callKeep
.on<CallKeepDidPerformSetMutedCallAction>(didPerformSetMutedCallAction);
_callKeep.on<CallKeepPerformEndCallAction>(endCall);
_callKeep.on<CallKeepPushKitToken>(onPushKitToken);

_callKeep.setup(context, <String, dynamic>{
'ios': {
'appName': 'CallKeepDemo',
},
'android': {
'alertTitle': 'Permissions required',
'alertDescription':
'This application needs to access your phone accounts',
'cancelButton': 'Cancel',
'okButton': 'ok',
'foregroundService': {
'channelId': 'com.company.my',
'channelName': 'Foreground service for my app',
'notificationId': 5005,
'notificationTitle': 'My app is running on background',
'notificationIcon': 'Path to the resource icon of the notification',
_callKeep.setup(
showAlertDialog: () => showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Permissions Required'),
content: const Text(
'This application needs to access your phone accounts'),
actions: <Widget>[
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(false),
),
TextButton(
child: const Text('OK'),
onPressed: () => Navigator.of(context).pop(true),
),
],
);
},
).then((value) => value ?? false),
options: <String, dynamic>{
'ios': {
'appName': 'CallKeepDemo',
},
'android': {
'additionalPermissions': [
'android.permission.CALL_PHONE',
'android.permission.READ_PHONE_NUMBERS'
],
'foregroundService': {
'channelId': 'com.example.call-kit-test',
'channelName': 'callKitTest',
'notificationTitle': 'My app is running on background',
'notificationIcon': 'Path to the resource icon of the notification',
},
},
},
});
);

if (Platform.isIOS) iOSPermission();

Expand All @@ -394,8 +417,8 @@ class MyAppState extends State<HomePage> {
calls[callUUID] = Call(callerId);
});
_callKeep.displayIncomingCall(
callUUID,
callerId,
uuid: callUUID,
handle: callerId,
callerName: callerName,
hasVideo: hasVideo,
);
Expand Down
1 change: 0 additions & 1 deletion ios/Classes/CallKeep.m
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,6 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush
- (NSString *)createUUID {
CFUUIDRef uuidObject = CFUUIDCreate(kCFAllocatorDefault);
NSString *uuidStr = (NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuidObject));
CFUUIDBytes bytes = CFUUIDGetUUIDBytes(uuidObject);
CFRelease(uuidObject);
return [uuidStr lowercaseString];
}
Expand Down
Loading

0 comments on commit 0377bfd

Please sign in to comment.