Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

onCallAccepted and onCallRejected events from onBackgroundMessage #6

Closed
chetansharmapsi opened this issue Apr 26, 2021 · 13 comments
Closed

Comments

@chetansharmapsi
Copy link

chetansharmapsi commented Apr 26, 2021

Hello,

I am trying to integration this awesome solution. I am able to integration incoming call notifications. But not able to receive onCallAccepted and onCallRejected events when app in is background and processCallNotification started form onBackgroundMessage. Do we need to do some extra configurations to able to receive onCallAccepted and onCallRejected events when app in is background? Thanks.

@TatankaConCube
Copy link
Contributor

for the current moment we have only one callback onCallAcceptedWhenTerminated (yes, there we have mistaken with callback naming and this callback calls when the call was rejected), which you can to listen when the app is in 'terminated' state. For which needs you want to listen onCallAccepted in the terminated state? After accepting the call from notification the application will be started and you can make the required work.

@chetansharmapsi
Copy link
Author

@TatankaConCube thanks for replying. Yes this is where I got stuck, I am not able to receive onCallAcceptedWhenTerminated callback when app is in terminated state OR in background (on accept and reject call button click from notification). Also after accepting the call, app not starting. Can you please guide if this require any specific initial setup.

@TatankaConCube
Copy link
Contributor

we provided the sample app, where we implemented behavior similar to yours, did you check it?

@chetansharmapsi
Copy link
Author

we provided the sample app, where we implemented behavior similar to yours, did you check it?

@TatankaConCube yes I checked that, followed same steps. But not able to detect root cause of the behavior at my side. May be it is because of my old flutter version.

@TatankaConCube
Copy link
Contributor

hm, I do not think so. Did you do any changes to your AndroidManifest.xml file?

@chetansharmapsi
Copy link
Author

hm, I do not think so. Did you do any changes to your AndroidManifest.xml file?

As app was not opening after clicking on Accept OR Reject notification action buttons, so I tried to add action_call_accept intent filters in manifest, but with this also no success.

@chetansharmapsi
Copy link
Author

I am not sure but I suspect that this issue is because of old flutter version and old firebase_messaging library version. as there are some issue in onBackgroundMessage handling in firebase_messaging: ^7.0.3.
So closing this issue.

@sanjay23singh
Copy link

@chetansharmapsi can u explain how u used this, i am still not able to get over this package.

@mayder
Copy link

mayder commented Feb 12, 2022

After updating to version 2.0.1 I have the same problem.
The app does not open after clicking "Accept" when it is closed.
But in Logcat it is possible to see prints that were placed inside "onCallAcceptedWhenTerminated"

This is my log in logcat:

2022-02-12 00:53:01.474 4264-4264/net.toon I/FLTFireMsgService: FlutterFirebaseMessagingBackgroundService started! 2022-02-12 00:53:02.461 4264-4264/net.toon D/NotificationsManager: customRingtone custom_ringtone 2022-02-12 00:53:02.461 4264-4264/net.toon D/NotificationsManager: ringtone 1 android.resource://net.toon/raw/custom_ringtone 2022-02-12 00:53:02.461 4264-4264/net.toon D/NotificationsManager: ringtone 2 android.resource://net.toon/raw/custom_ringtone 2022-02-12 00:53:06.134 4264-4264/net.toon I/EventReceiver: NotificationReceiver onReceive Call ACCEPT, callId: 1644637969500977 2022-02-12 00:53:06.171 4264-4407/net.toon I/ConnectycubeFlutterBgPerformingService: Service has not yet started, messages will be queued. 2022-02-12 00:53:06.177 4264-4303/net.toon W/FlutterJNI: FlutterJNI.loadLibrary called more than once 2022-02-12 00:53:06.181 4264-4300/net.toon W/FlutterJNI: FlutterJNI.prefetchDefaultFontManager called more than once 2022-02-12 00:53:06.189 4264-4407/net.toon I/ResourceExtractor: Found extracted resources res_timestamp-22-1644636804785 2022-02-12 00:53:06.196 4264-4264/net.toon W/FlutterJNI: FlutterJNI.init called more than once 2022-02-12 00:53:06.203 4264-4264/net.toon I/FlutterConnectycubeBackgroundExecutor: Creating background FlutterEngine instance. 2022-02-12 00:53:08.190 4264-4264/net.toon I/ConnectycubeFlutterBgPerformingService: ConnectycubeFlutterBgPerformingService started! 2022-02-12 00:53:08.338 4264-4408/net.toon I/flutter: [CallEvent.fromMap] map: {caller_name: Breno Mayder , user_info: {"origem":"call_voz"}, caller_id: 2, session_id: 1644637969500977, call_opponents: 2,3, call_type: 2} 2022-02-12 00:53:08.427 4264-4408/net.toon I/flutter: Test Print

In version 0.1.0-dev.1 my app opened

@TatankaConCube
Copy link
Contributor

@mayder the root cause was localized, the issue will be fixed in the next release

@TatankaConCube
Copy link
Contributor

@mayder the version 2.0.2 was released, try it

@mayder
Copy link

mayder commented Feb 14, 2022

Ok, Thanks

@Faisalbutt555
Copy link

Faisalbutt555 commented Dec 9, 2023

@TatankaConCube i am trying to accept call or reject call on terminate state but its not pick or reject can u help me out this
here is my class how can i acheive it

/// Function type for handling accepted and rejected call events
typedef CallEventHandler = Future Function(CallEvent event);

/// {@template connectycube_flutter_call_kit}
/// Plugin to manage call events and notifications
/// {@endtemplate}
class ConnectycubeFlutterCallKit {
static const MethodChannel _methodChannel =
const MethodChannel('connectycube_flutter_call_kit.methodChannel');
static const EventChannel _eventChannel =
const EventChannel('connectycube_flutter_call_kit.callEventChannel');

/// {@macro connectycube_flutter_call_kit}
factory ConnectycubeFlutterCallKit() => _getInstance();
const ConnectycubeFlutterCallKit._internal();

static ConnectycubeFlutterCallKit get instance => _getInstance();
static ConnectycubeFlutterCallKit? _instance;
static String TAG = "ConnectycubeFlutterCallKit";

static ConnectycubeFlutterCallKit _getInstance() {
if (_instance == null) {
_instance = ConnectycubeFlutterCallKit._internal();
}
return _instance!;
}

static int _bgHandler = -1;

static Function(String newToken)? onTokenRefreshed;

/// iOS only callbacks
static Function(bool isMuted, String sessionId)? onCallMuted;

/// end iOS only callbacks

static CallEventHandler? _onCallRejectedWhenTerminated;
static CallEventHandler? _onCallAcceptedWhenTerminated;

static CallEventHandler? _onCallAccepted;
static CallEventHandler? _onCallRejected;

/// Initialize the plugin and provided user callbacks.
///
/// - This function should only be called once at the beginning of
/// your application.

void init(
{CallEventHandler? onCallAccepted,
CallEventHandler? onCallRejected,
CallEventHandler? onCallRejectedWhenTerminated,
CallEventHandler? onCallAcceptedWhenTerminated,
String? ringtone,
String? icon,
@deprecated('Use AndroidManifest.xml meta-data instead')
String? notificationIcon,
String? color}) {
_onCallAccepted = onCallAccepted;
_onCallRejected = onCallRejected;

updateConfig(
    ringtone: ringtone,
    icon: icon,
    color: color);
initEventsHandler();
ConnectycubeFlutterCallKit.onCallRejectedWhenTerminated = _onCallRejectedWhenTerminated;
ConnectycubeFlutterCallKit.onCallAcceptedWhenTerminated = _onCallAcceptedWhenTerminated;

}

/// Set a reject call handler function which is called when the app is in the
/// background or terminated.
///
/// This provided handler must be a top-level function and cannot be
/// anonymous otherwise an [ArgumentError] will be thrown.
@pragma('vm:entry-point')
static set onCallRejectedWhenTerminated(CallEventHandler? handler) {
_onCallRejectedWhenTerminated = handler;
if (handler != null) {
instance._registerBackgroundCallEventHandler(
handler, BackgroundCallbackName.REJECTED_IN_BACKGROUND);
}
}

/// Set a accept call handler function which is called when the app is in the
/// background or terminated.
///
/// This provided handler must be a top-level function and cannot be
/// anonymous otherwise an [ArgumentError] will be thrown.
@pragma('vm:entry-point')
static set onCallAcceptedWhenTerminated(CallEventHandler? handler) {
_onCallAcceptedWhenTerminated = handler;
if (handler != null) {
instance._registerBackgroundCallEventHandler(
handler, BackgroundCallbackName.ACCEPTED_IN_BACKGROUND);
}
}

@pragma('vm:entry-point')
Future _registerBackgroundCallEventHandler(
CallEventHandler handler, String callbackName) async {
if (!Platform.isAndroid) {
return;
}

if (_bgHandler == -1) {
  final CallbackHandle bgHandle = PluginUtilities.getCallbackHandle(
      _backgroundEventsCallbackDispatcher)!;

  _bgHandler = bgHandle.toRawHandle();
}

final CallbackHandle userHandle =
    PluginUtilities.getCallbackHandle(handler)!;

await _methodChannel.invokeMapMethod('startBackgroundIsolate', {
  'pluginCallbackHandle': _bgHandler,
  'userCallbackHandleName': callbackName,
  'userCallbackHandle': userHandle.toRawHandle(),
});

}
@pragma('vm:entry-point')
static void initEventsHandler() {
_eventChannel.receiveBroadcastStream().listen((rawData) {
print('[initEventsHandler] rawData: $rawData');
final eventData = Map<String, dynamic>.from(rawData);
_processEvent(eventData);

});

}

/// Sets the additional configs for the Call notification
/// [ringtone] - the name of the ringtone source (for Anfroid it should be placed by path 'res/raw', for iOS it is a name of ringtone)
/// [icon] - the name of image in the drawable folder for Android and the name of Assests set for iOS
/// [notificationIcon] - the name of the image in the drawable folder, uses as Notification Small Icon for Android, ignored for iOS
/// [color] - the color in the format '#RRGGBB', uses as an Android Notification accent color, ignored for iOS
Future updateConfig(
{String? ringtone,
String? icon,
@deprecated('Use AndroidManifest.xml meta-data instead')
String? notificationIcon,
String? color}) {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod('updateConfig', {
  'ringtone': ringtone,
  'icon': icon,
  'notification_icon': notificationIcon,
  'color': color,
});

}

/// Returns VoIP token for iOS plaform.
/// Returns FCM token for Android platform
static Future<String?> getToken() {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value(null);

return _methodChannel.invokeMethod('getVoipToken', {}).then((result) {
  return result?.toString();
});

}

/// Show incoming call notification
@pragma('vm:entry-point')
static Future showCallNotification(CallEvent callEvent) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod(
    "showCallNotification", callEvent.toMap());

}

/// Report that the current active call has been accepted by your application
///
static Future reportCallAccepted({required String? sessionId}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel
    .invokeMethod("reportCallAccepted", {'session_id': sessionId});

}

/// Report that the current active call has been ended by your application
static Future reportCallEnded({
required String? sessionId,
}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod("reportCallEnded", {
  'session_id': sessionId,
});

}

/// Get the current call state
///
/// Other platforms than Android and iOS will receive [CallState.UNKNOWN]
static Future getCallState({
required String? sessionId,
}) async {
if (!Platform.isAndroid && !Platform.isIOS)
return Future.value(CallState.UNKNOWN);

return _methodChannel.invokeMethod("getCallState", {
  'session_id': sessionId,
}).then((state) {
  return state.toString();
});

}

/// Updates the current call state
static Future setCallState({
required String? sessionId,
required String? callState,
}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod("setCallState", {
  'session_id': sessionId,
  'call_state': callState,
});

}

/// Retrieves call information about the call
static Future<Map<String, dynamic>?> getCallData({
required String? sessionId,
}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value(null);

return _methodChannel.invokeMethod("getCallData", {
  'session_id': sessionId,
}).then((data) {
  if (data == null) {
    return Future.value(null);
  }
  return Future.value(Map<String, dynamic>.from(data));
});

}

/// Cleans all data related to the call
static Future clearCallData({
required String? sessionId,
}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod("clearCallData", {
  'session_id': sessionId,
});

}

/// Returns the id of the last displayed call.
/// It is useful on starting app step for navigation to the call screen if the call was accepted
static Future<String?> getLastCallId() async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value(null);

return _methodChannel.invokeMethod("getLastCallId");

}

static Future setOnLockScreenVisibility({
required bool? isVisible,
}) async {
if (!Platform.isAndroid) return;

return _methodChannel.invokeMethod("setOnLockScreenVisibility", {
  'is_visible': isVisible,
});

}

/// Report that the current active call has been ended by your application
static Future reportCallMuted(
{required String? sessionId, required bool? muted}) async {
if (!Platform.isAndroid && !Platform.isIOS) return Future.value();

return _methodChannel.invokeMethod("muteCall", {
  'session_id': sessionId,
  'muted': muted,
});

}

/// Returns whether the app can send fullscreen intents (required for showing
/// the Incoming call screen on the Lockscreen)
static Future canUseFullScreenIntent() async {
if (!Platform.isAndroid) return Future.value(true);

return _methodChannel.invokeMethod("canUseFullScreenIntent").then((result) {
  if (result == null) {
    return false;
  }

  return result;
});

}

/// Opens the Setting to grant/deny permission for running the fullscreen Intents
static Future provideFullScreenIntentAccess() async {
if (!Platform.isAndroid) return Future.value();

return _methodChannel.invokeMethod("provideFullScreenIntentAccess");

}

static void _processEvent(Map<String, dynamic> eventData)async {
// log('[ConnectycubeFlutterCallKit][_processEvent] eventData: $eventData');

var event = eventData["event"] as String;
var arguments = Map<String, dynamic>.from(eventData['args']);

switch (event) {
  case 'voipToken':
    onTokenRefreshed?.call(arguments['voipToken']);
    break;

  case 'answerCall':
     var callEvent = CallEvent.fromMap(arguments);
      _onCallAccepted?.call(callEvent);

await ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);

   FirestoreDataProviderCALLHISTORY().callpickup();
  print("answer call provider set");


    break;
  case 'endCall':
   print("chal decline");
    var callEvent = CallEvent.fromMap(arguments);
     _onCallRejected?.call(callEvent);
    ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
      await Firebase.initializeApp();
      SharedPreferences sh = await SharedPreferences.getInstance();
      var myphotoUrl = sh.getString(Dbkeys.photoUrl) ?? '';
      var mynickname = sh.getString(Dbkeys.nickname) ?? '';
      var calltype = sh.getString('calltype');
      final collectionRef = await FirebaseFirestore.instance
          .collection(DbPaths.collectionusers)
          .doc(mynickname)
          .collection(DbPaths.collectioncallhistory);
      final snapshot = await collectionRef.get();
      final List<String> docIds = [];
      snapshot.docs.forEach((doc) {
        docIds.add(doc.id);
      });
      int intValue = int.parse(docIds.last);
      final CallMethods callMethods = CallMethods();
      Call call = Call(
          timeepoch: intValue,
          callerId: callcv,
          callerName: callcv,
          callerPic: '',
          hasDialled: false,
          receiverId: sh.getString(Dbkeys.phone),
          receiverName: mynickname,
          receiverPic: myphotoUrl,
          channelId: Random().nextInt(1000).toString(),
          isvideocall: calltype == "Incoming Video Call..." ? true : false);
      await callMethods.endCall(call: call);
      FirebaseFirestore.instance
          .collection(DbPaths.collectionusers)
          .doc(call.callerId)
          .collection(DbPaths.collectioncallhistory)
          .doc(intValue.toString())
          .set({
        'STATUS': 'rejected',
        'ENDED': DateTime.now(),
      }, SetOptions(merge: true));
      FirebaseFirestore.instance
          .collection(DbPaths.collectionusers)
          .doc(call.receiverId)
          .collection(DbPaths.collectioncallhistory)
          .doc(intValue.toString())
          .set({
        'STATUS': 'rejected',
        'ENDED': DateTime.now(),
      }, SetOptions(merge: true));
      //----------
      await FirebaseFirestore.instance
          .collection(DbPaths.collectionusers)
          .doc(call.callerId)
          .collection('recent')
          .doc('callended')
          .delete();
      Future.delayed(const Duration(milliseconds: 200), () async {
        await FirebaseFirestore.instance
            .collection(DbPaths.collectionusers)
            .doc(call.callerId)
            .collection('recent')
            .doc('callended')
            .set({'id': call.callerId, 'ENDED': DateTime.now()});
      });

      FirestoreDataProviderCALLHISTORY().fetchNextData(
          'CALLHISTORY',
          FirebaseFirestore.instance
              .collection(DbPaths.collectionusers)
              .doc(call.receiverId)
              .collection(DbPaths.collectioncallhistory)
              .orderBy('TIME', descending: true)
              .limit(14),
          true);
    break;

  case 'startCall':
    break;

  case 'setMuted':
    onCallMuted?.call(true, arguments["session_id"]);
    break;

  case 'setUnMuted':
    onCallMuted?.call(false, arguments["session_id"]);
    break;

  case '':
    break;

  default:
    throw Exception("Unrecognized event");
}

}
}

// This is the entrypoint for the background isolate. Since we can only enter
// an isolate once, we setup a MethodChannel to listen for method invocations
// from the native portion of the plugin. This allows for the plugin to perform
// any necessary processing in Dart (e.g., populating a custom object) before
// invoking the provided callback.
@pragma('vm:entry-point')
void _backgroundEventsCallbackDispatcher() {
// Initialize state necessary for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();

const MethodChannel _channel = MethodChannel(
'connectycube_flutter_call_kit.methodChannel.background',
);

// This is where we handle background events from the native portion of the plugin.
_channel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'onBackgroundEvent') {
final CallbackHandle handle =
CallbackHandle.fromRawHandle(call.arguments['userCallbackHandle']);

  // PluginUtilities.getCallbackFromHandle performs a lookup based on the
  // callback handle and returns a tear-off of the original callback.
  final callback = PluginUtilities.getCallbackFromHandle(handle)!
      as Future<void> Function(CallEvent);

  try {
    Map<String, dynamic> callEventMap =
        Map<String, dynamic>.from(call.arguments['args']);
    final CallEvent callEvent = CallEvent.fromMap(callEventMap);
    await callback(callEvent);
  } catch (e) {
    // ignore: avoid_print
    // log('[ConnectycubeFlutterCallKit][_backgroundEventsCallbackDispatcher] An error occurred in your background event handler: $e');
    // ignore: avoid_print
  }
} else {
  throw UnimplementedError('${call.method} has not been implemented');
}

});

// Once we've finished initializing, let the native portion of the plugin
// know that it can start scheduling alarms.
_channel.invokeMethod('onBackgroundHandlerInitialized');
}

class CallState {
static const String PENDING = "pending";
static const String ACCEPTED = "accepted";
static const String REJECTED = "rejected";
static const String UNKNOWN = "unknown";
}

class BackgroundCallbackName {
static const String REJECTED_IN_BACKGROUND = "rejected_in_background";
static const String ACCEPTED_IN_BACKGROUND = "accepted_in_background";
}

here i am calling it in my home screen
String? opponentIdString = sharedPreferences.getString('loginId');
int opponentId = opponentIdString != null ? int.parse(opponentIdString) : 0;
Set opponentsIds = {opponentId};
int callerid= int.parse(bodyMultilang);;
Uuid uuid = Uuid();
CallEvent callEvent = CallEvent(
sessionId: uuid.v4() ,
callType: title == "Incoming Video Call..." ? 1 : 0,
callerId:callerid,
callerName: bodyMultilang!,
opponentsIds:opponentsIds,
callPhoto: dp ?? '',
userInfo: {'customParameter1': 'value1'});
ConnectycubeFlutterCallKit.onCallRejectedWhenTerminated = onCallRejectedWhenTerminated;
ConnectycubeFlutterCallKit.onCallAcceptedWhenTerminated = onCallAcceptedWhenTerminated;
ConnectycubeFlutterCallKit.instance.init(
onCallAcceptedWhenTerminated: onCallAcceptedWhenTerminated,
onCallRejectedWhenTerminated: onCallRejectedWhenTerminated
);
ConnectycubeFlutterCallKit.showCallNotification(callEvent);
ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants