ConnectyCube / connectycube-flutter-call-kit

A Flutter plugin for displaying call screen when the app in the background or terminated.
https://developers.connectycube.com/flutter
Apache License 2.0
57 stars 77 forks source link

onCallAccepted and onCallRejected events from onBackgroundMessage #6

Closed chetansharmapsi closed 3 years ago

chetansharmapsi commented 3 years ago

Hello,

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

TatankaConCube commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

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

chetansharmapsi commented 3 years ago

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 commented 3 years ago

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

chetansharmapsi commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

mayder commented 2 years ago

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 commented 2 years ago

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

TatankaConCube commented 2 years ago

@mayder the version 2.0.2 was released, try it

mayder commented 2 years ago

Ok, Thanks

Faisalbutt555 commented 9 months ago

@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);