android - Flutter ConnectyCube: Accepting Call from Notification Doesn't Redirect to App - Stack Overflow

Problem:I'm using ConnectyCube in my Flutter app for handling video calls. When a call is initiat

Problem:

I'm using ConnectyCube in my Flutter app for handling video calls. When a call is initiated from one device to another and the receiving device is in the background, the call notification appears as expected.

However, when I tap "Accept" on the notification, the call is accepted but the app does not open. On the other hand, if I tap the notification itself, the app opens as expected.

Expected Behavior:

When I click on "Accept" in the call notification, it should:

Accept the call Redirect/open the app Current Behavior:

Clicking "Accept" accepts the call but does not bring the app to the foreground. Clicking the notification itself correctly opens the app. What I’ve Tried:

Ensuring the app has the necessary foreground service permissions. Checking if launchMode is correctly set in AndroidManifest.xml. Looking into how ConnectyCube handles background notifications and app redirection. Question:

How can I ensure that when I tap "Accept" on the notification, the app is brought to the foreground?

Any help or guidance would be appreciated!

below is my AndroidManifest file

    xmlns:tools=";

    package="com.sambuq.care_first">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    
    <!-- End FlutterDownloader customization -->
    <!-- Provide required visibility configuration for API level 30 and above -->
    <queries>
    <!-- If your app checks for SMS support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="sms" />
    </intent>
    <!-- If your app checks for call support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="tel" />
    </intent>

    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
    </queries>

   <application
        android:label="360CareFirst"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <provider
            android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
            android:authorities="${applicationId}.flutter_downloader.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
        <!-- ... (other configurations) -->
        <!-- Begin FlutterDownloader customization -->
        <!-- disable default Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>

        <!-- declare customized Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
            android:authorities="${applicationId}.flutter-downloader-init"
            android:exported="false">
            <meta-data
                android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
                android:value="1" />
        </provider> 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

below is call manager that I use

import 'package:care_first/bloc/auth_bloc/bloc/auth_bloc.dart';
import 'package:care_first/routing/route_names.dart';
import 'package:care_first/screens/my_360_provider/services/fcm/fcm_setup.dart';
import 'package:care_first/screens/my_360_provider/services/fcm/notification_handler.dart';
import 'package:care_first/screens/settings/my_appointments/doctor_appointment/models/my_appointment.dart';
import 'package:care_first/shared/helper/helper_methods.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';

import 'call_kit_manager.dart';
import '../utils/consts.dart';

class CallManager {
  static String TAG = "CallManager";

  static CallManager get instance => _getInstance();
  static CallManager? _instance;

  static CallManager _getInstance() {
    return _instance ??= CallManager._internal();
  }

  factory CallManager() => _getInstance();

  CallManager._internal();

  P2PClient? _callClient;
  P2PSession? _currentCall;
  BuildContext? context;
  MediaStream? localMediaStream;
  Map<int, MediaStream> remoteStreams = {};
  GlobalKey<NavigatorState>? navigatorKey;
  String? selfCubeId;
  late String resourceId;
  SystemMessagesManager? systemMessagesManager =
      CubeChatConnection.instance.systemMessagesManager;
  CubeMessage systemMessage = CubeMessage();
  final player = AudioPlayer();

  Function(bool, String)? onMicMuted;

  init(
    BuildContext context,
    GlobalKey<NavigatorState> navigatorKey,
    String selfCubeId,
    String resourceId,
  ) {
    this.context = context;
    this.navigatorKey = navigatorKey;
    this.selfCubeId = selfCubeId;
    this.resourceId = resourceId;

    _initCustomMediaConfigs();

    if (CubeChatConnection.instance.isAuthenticated()) {
      _initCalls(context);
    } else {
      _initChatConnectionStateListener(context);
    }

    _initCallKit();
  }

  destroy() {
    _callClient?.destroy();
    _callClient = null;
  }

  void _initCustomMediaConfigs() {
    RTCMediaConfig mediaConfig = RTCMediaConfig.instance;
    mediaConfig.minHeight = 340;
    mediaConfig.minWidth = 480;
    mediaConfig.minFrameRate = 25;

    RTCConfig.instance.statsReportsInterval = 200;
  }

  void _initCalls(BuildContext context) {
    if (_callClient == null) {
      _callClient = P2PClient.instance;

      _callClient!.init();
    }
    // _callClient is P2PClient
    _callClient!.onReceiveNewSession = (callSession) async {
      print("${'*' * 10} RECEIVED NEW CALL SESSION ${'*' * 10}");
      // if (_currentCall != null &&
      //     _currentCall!.sessionId != callSession.sessionId &&
      //     _currentCall!.opponentsIds.first.toString() != selfCubeId) {
      //   callSession.reject();
      //   return;
      // }
      if (navigatorKey!.currentContext!.read<AuthBloc>().user.persona ==
          "patient") {
        await player
            .setReleaseMode(ReleaseMode.loop)
            .then(
                (value) => player.setSource(AssetSource('audio/ringtone.mp3')))
            .then((value) => player.resume());
      }

      _currentCall = callSession;

      var callState = await _getCallState(_currentCall!.sessionId);
      await Future.delayed(Duration(seconds: 1));
      if (callState == CallState.REJECTED) {
        reject(_currentCall!.sessionId, false);
      } else if (callState == CallState.ACCEPTED) {
        acceptCall(_currentCall!.sessionId,
            _currentCall?.cubeSdp.userInfo ?? {}, false);
      } else if (callState == CallState.UNKNOWN ||
          callState == CallState.PENDING) {
        if (callState == CallState.UNKNOWN &&
            (Platform.isIOS || Platform.isAndroid)) {
          await ConnectycubeFlutterCallKit.setCallState(
              sessionId: _currentCall!.sessionId, callState: CallState.PENDING);
        }

        if (_currentCall!.cubeSdp.callerId.toString() != selfCubeId) {
          navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
              callSession;
          // await Future.delayed(Duration(seconds: 1));
          // await ConnectycubeFlutterCallKit.showCallNotification(CallEvent(
          //   sessionId: _currentCall!.sessionId,
          //   callType: CallType.VIDEO_CALL,
          //   callerId: _currentCall!.cubeSdp.callerId,
          //   callerName: "YASH",
          //   opponentsIds: _currentCall!.opponentsIds,
          //   userInfo: _currentCall?.cubeSdp.userInfo ?? {},
          // ));
          print("NOTIFICATION NOT CALLED");
          _showIncomingCallScreen(_currentCall!, navigatorKey!.currentContext!);
        }
      }

      _currentCall?.onLocalStreamReceived = (localStream) {
        localMediaStream = localStream;
      };

      _currentCall?.onRemoteStreamReceived = (session, userId, stream) {
        remoteStreams[userId] = stream;
      };

      _currentCall?.onRemoteStreamRemoved = (session, userId, stream) {
        remoteStreams.remove(userId);
      };

      _currentCall?.onReceiveHungUpFromUser =
          (session, userId, [userInfo]) async {
        print("CALL_MANAGER: onReceiveHungUpFromUser");
        /* systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_HUNGUP";
        print("-------------BROADCASTING CALL HUNGUP EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
        broadcastSystemMessage("CALL_HUNGUP");
        await player.release();
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      };
    };

    _callClient!.onSessionClosed = (callSession) async {
      if (_currentCall != null &&
          _currentCall!.sessionId == callSession.sessionId) {
        _currentCall = null;
        localMediaStream?.getTracks().forEach((track) async {
          await track.stop();
        });
        await localMediaStream?.dispose();
        localMediaStream = null;

        remoteStreams.forEach((key, value) async {
          await value.dispose();
        });

        remoteStreams.clear();
        CallKitManager.instance.processCallFinished(callSession.sessionId);
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      }
    };
  }

  void startNewCall(BuildContext context, int callType, int patientCubeId,
      MyAppointment appointment, String? businessId, String? moduleId) async {
    this.context = context;
    Set<int> opponents = {patientCubeId};
    if (opponents.isEmpty) return;
    if (!kIsWeb) {
      if (Platform.isIOS) {
        await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
            preferSpeakerOutput: true);
      }
    }
    P2PSession callSession =
        await _callClient!.createCallSession(callType, opponents);
    _currentCall = callSession;
    // storing it in authbloc
    context.read<AuthBloc>().p2pSession = callSession;
    context.read<AuthBloc>().resourceId = resourceId;
    context.read<AuthBloc>().selfCubeId = selfCubeId;

    String callerName;
    if (appointment.doctorMap?["firstname"] != null) {
      callerName =
          "${appointment.doctorMap?["firstname"]} ${appointment.doctorMap?["lastname"]}";
    } else {
      callerName = "";
    }

    await _sendStartCallSignalForOffliners(_currentCall!, callerName);
    GoRouter.of(context)
        .pushNamed(RoutesName.videoCallDoctorRoute, pathParameters: {
      'businessId': encrypt(text: businessId ?? '', context: context),
    }, queryParameters: {
      'moduleId': moduleId,
    }, extra: {
      // 'callSession': callSession,
      'appointment': appointment,
    });
  }

  void _showIncomingCallScreen(P2PSession callSession, BuildContext context) {
    if (navigatorKey!.currentContext != null) {
      GoRouter.of(navigatorKey!.currentContext!).pushNamed(
          RoutesName.incomingVideoCall,
          extra: Map<String, String>.from(
              {"resourceId": resourceId, "selfCubeId": selfCubeId}));
    }
  }

  void acceptCall(
    String sessionId,
    Map<String, String> userInfo,
    bool fromCallkit,
  ) async {
    await player.release();
    log('acceptCall, from callKit: $fromCallkit', TAG);
    //ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);
    if (_currentCall != null) {
      if (context != null) {
        // if (AppLifecycleState.resumed !=
        //     WidgetsBinding.instance.lifecycleState) {
        await _currentCall?.acceptCall();
        // }
        MyAppointment appointment = MyAppointment(
            id: userInfo['appointment_id'], doctor: userInfo['doctor_id']);
        log(userInfo.toString(), "User Info from AcceptCall");
        if (!fromCallkit) {
          await ConnectycubeFlutterCallKit.reportCallAccepted(
              sessionId: sessionId);
        }
        broadcastSystemMessage("CALL_ACCEPTED");
        navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
            _currentCall;
        GoRouter.of(navigatorKey!.currentContext!).pushReplacementNamed(
          RoutesName.videoCallPatientRoute,
          extra: Map<String, Object>.from({
            // 'currentCall': _currentCall!,
            'appointment': appointment,
          }),
        );

        /*  systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_ACCEPTED";
        print("-------------BROADCASTING ACCEPT CALL EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
      }
      if (!kIsWeb) {
        if (Platform.isIOS) {
          await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
              preferSpeakerOutput: true);
        }
      }
    }
  }

  void reject(String sessionId, bool fromCallkit) {
    if (_currentCall != null) {
      player.release();
      broadcastSystemMessage("CALL_REJECTED");
      if (fromCallkit) {
        ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
      } else {
        CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      }

      _currentCall!.reject();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  void hungUp() {
    if (_currentCall != null) {
      player.release();
      CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      _currentCall!.hungUp();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  CreateEventParams _getCallEventParameters(
      P2PSession currentCall, String? callerName) {
    /* String? callerName = users
        .where((cubeUser) => cubeUser.id == currentCall.callerId)
        .first
        .fullName; */
    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message':
          "Incoming ${currentCall.callType == CallType.VIDEO_CALL ? "Video" : "Audio"} call",
      PARAM_CALL_TYPE: currentCall.callType,
      PARAM_SESSION_ID: currentCall.sessionId,
      PARAM_CALLER_ID: currentCall.callerId,
      PARAM_CALL_OPPONENTS: currentCall.opponentsIds.join(','),
      PARAM_CALLER_NAME: callerName ?? "Doctor",
    };

    params.notificationType = NotificationType.PUSH;
    params.environment = CubeEnvironment.DEVELOPMENT;
    //kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = currentCall.opponentsIds.toList();

    return params;
  }

  Future<void> _sendStartCallSignalForOffliners(
      P2PSession currentCall, String callerName) async {
    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_START_CALL;
    params.parameters[PARAM_IOS_VOIP] = 1;
    params.parameters[PARAM_EXPIRATION] = 0;
    params.parameters['ios_push_type'] = 'voip';

    await createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created: $cubeEvent");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _sendEndCallSignalForOffliners(
      P2PSession? currentCall, String? callerName) {
    if (currentCall == null) return;

    CubeUser? currentUser = CubeChatConnection.instance.currentUser;
    if (currentUser == null || currentUser.id != currentCall.callerId) return;

    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_END_CALL;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _initCallKit() {
    CallKitManager.instance.init(
      onCallAccepted: (uuid) {
        acceptCall(uuid, _currentCall?.cubeSdp.userInfo ?? {}, true);
      },
      onCallEnded: (uuid) {
        reject(uuid, true);
      },
      onMuteCall: (mute, uuid) {
        onMicMuted?.call(mute, uuid);
      },
    );
  }

  void _initChatConnectionStateListener(BuildContext context) {
    CubeChatConnection.instance.connectionStateStream.listen((state) {
      if (CubeChatConnectionState.Ready == state) {
        _initCalls(context);
      }
    });
  }

  Future<String> _getCallState(String sessionId) async {
    if (Platform.isAndroid || Platform.isIOS) {
      var callState =
          await ConnectycubeFlutterCallKit.getCallState(sessionId: sessionId);

      log("CONECTICUBE CALL STATE: $callState");
      return callState;
    } else {
      return Future.value(CallState.UNKNOWN);
    }
  }

  void muteCall(String sessionId, bool mute) {
    CallKitManager.instance.muteCall(sessionId, mute);
  }

  Future<bool> _onBackPressed(BuildContext context) {
    return Future.value(false);
  }

  void broadcastSystemMessage(action) {
    systemMessage.recipientId = int.tryParse(selfCubeId!);
    systemMessage.properties["resourceId"] = resourceId;
    systemMessage.properties["action"] = action;
    print("BROADCASTING: $action EVENT");
    systemMessagesManager?.sendSystemMessage(systemMessage);
  }
}

Problem:

I'm using ConnectyCube in my Flutter app for handling video calls. When a call is initiated from one device to another and the receiving device is in the background, the call notification appears as expected.

However, when I tap "Accept" on the notification, the call is accepted but the app does not open. On the other hand, if I tap the notification itself, the app opens as expected.

Expected Behavior:

When I click on "Accept" in the call notification, it should:

Accept the call Redirect/open the app Current Behavior:

Clicking "Accept" accepts the call but does not bring the app to the foreground. Clicking the notification itself correctly opens the app. What I’ve Tried:

Ensuring the app has the necessary foreground service permissions. Checking if launchMode is correctly set in AndroidManifest.xml. Looking into how ConnectyCube handles background notifications and app redirection. Question:

How can I ensure that when I tap "Accept" on the notification, the app is brought to the foreground?

Any help or guidance would be appreciated!

below is my AndroidManifest file

    xmlns:tools="http://schemas.android/tools"

    package="com.sambuq.care_first">
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" tools:node="remove"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    
    <!-- End FlutterDownloader customization -->
    <!-- Provide required visibility configuration for API level 30 and above -->
    <queries>
    <!-- If your app checks for SMS support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="sms" />
    </intent>
    <!-- If your app checks for call support -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="tel" />
    </intent>

    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
    </queries>

   <application
        android:label="360CareFirst"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <provider
            android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
            android:authorities="${applicationId}.flutter_downloader.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>
        <!-- ... (other configurations) -->
        <!-- Begin FlutterDownloader customization -->
        <!-- disable default Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="androidx.startup.InitializationProvider"
            android:authorities="${applicationId}.androidx-startup"
            android:exported="false"
            tools:node="merge">
            <meta-data
                android:name="androidx.work.WorkManagerInitializer"
                android:value="androidx.startup"
                tools:node="remove" />
        </provider>

        <!-- declare customized Initializer -->
        <!-- Remove the following <provider> element -->
        <provider
            android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
            android:authorities="${applicationId}.flutter-downloader-init"
            android:exported="false">
            <meta-data
                android:name="vn.hunghd.flutterdownloader.MAX_CONCURRENT_TASKS"
                android:value="1" />
        </provider> 
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <intent-filter>
                <action android:name="FLUTTER_NOTIFICATION_CLICK"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

below is call manager that I use

import 'package:care_first/bloc/auth_bloc/bloc/auth_bloc.dart';
import 'package:care_first/routing/route_names.dart';
import 'package:care_first/screens/my_360_provider/services/fcm/fcm_setup.dart';
import 'package:care_first/screens/my_360_provider/services/fcm/notification_handler.dart';
import 'package:care_first/screens/settings/my_appointments/doctor_appointment/models/my_appointment.dart';
import 'package:care_first/shared/helper/helper_methods.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:universal_io/io.dart';
import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';

import 'call_kit_manager.dart';
import '../utils/consts.dart';

class CallManager {
  static String TAG = "CallManager";

  static CallManager get instance => _getInstance();
  static CallManager? _instance;

  static CallManager _getInstance() {
    return _instance ??= CallManager._internal();
  }

  factory CallManager() => _getInstance();

  CallManager._internal();

  P2PClient? _callClient;
  P2PSession? _currentCall;
  BuildContext? context;
  MediaStream? localMediaStream;
  Map<int, MediaStream> remoteStreams = {};
  GlobalKey<NavigatorState>? navigatorKey;
  String? selfCubeId;
  late String resourceId;
  SystemMessagesManager? systemMessagesManager =
      CubeChatConnection.instance.systemMessagesManager;
  CubeMessage systemMessage = CubeMessage();
  final player = AudioPlayer();

  Function(bool, String)? onMicMuted;

  init(
    BuildContext context,
    GlobalKey<NavigatorState> navigatorKey,
    String selfCubeId,
    String resourceId,
  ) {
    this.context = context;
    this.navigatorKey = navigatorKey;
    this.selfCubeId = selfCubeId;
    this.resourceId = resourceId;

    _initCustomMediaConfigs();

    if (CubeChatConnection.instance.isAuthenticated()) {
      _initCalls(context);
    } else {
      _initChatConnectionStateListener(context);
    }

    _initCallKit();
  }

  destroy() {
    _callClient?.destroy();
    _callClient = null;
  }

  void _initCustomMediaConfigs() {
    RTCMediaConfig mediaConfig = RTCMediaConfig.instance;
    mediaConfig.minHeight = 340;
    mediaConfig.minWidth = 480;
    mediaConfig.minFrameRate = 25;

    RTCConfig.instance.statsReportsInterval = 200;
  }

  void _initCalls(BuildContext context) {
    if (_callClient == null) {
      _callClient = P2PClient.instance;

      _callClient!.init();
    }
    // _callClient is P2PClient
    _callClient!.onReceiveNewSession = (callSession) async {
      print("${'*' * 10} RECEIVED NEW CALL SESSION ${'*' * 10}");
      // if (_currentCall != null &&
      //     _currentCall!.sessionId != callSession.sessionId &&
      //     _currentCall!.opponentsIds.first.toString() != selfCubeId) {
      //   callSession.reject();
      //   return;
      // }
      if (navigatorKey!.currentContext!.read<AuthBloc>().user.persona ==
          "patient") {
        await player
            .setReleaseMode(ReleaseMode.loop)
            .then(
                (value) => player.setSource(AssetSource('audio/ringtone.mp3')))
            .then((value) => player.resume());
      }

      _currentCall = callSession;

      var callState = await _getCallState(_currentCall!.sessionId);
      await Future.delayed(Duration(seconds: 1));
      if (callState == CallState.REJECTED) {
        reject(_currentCall!.sessionId, false);
      } else if (callState == CallState.ACCEPTED) {
        acceptCall(_currentCall!.sessionId,
            _currentCall?.cubeSdp.userInfo ?? {}, false);
      } else if (callState == CallState.UNKNOWN ||
          callState == CallState.PENDING) {
        if (callState == CallState.UNKNOWN &&
            (Platform.isIOS || Platform.isAndroid)) {
          await ConnectycubeFlutterCallKit.setCallState(
              sessionId: _currentCall!.sessionId, callState: CallState.PENDING);
        }

        if (_currentCall!.cubeSdp.callerId.toString() != selfCubeId) {
          navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
              callSession;
          // await Future.delayed(Duration(seconds: 1));
          // await ConnectycubeFlutterCallKit.showCallNotification(CallEvent(
          //   sessionId: _currentCall!.sessionId,
          //   callType: CallType.VIDEO_CALL,
          //   callerId: _currentCall!.cubeSdp.callerId,
          //   callerName: "YASH",
          //   opponentsIds: _currentCall!.opponentsIds,
          //   userInfo: _currentCall?.cubeSdp.userInfo ?? {},
          // ));
          print("NOTIFICATION NOT CALLED");
          _showIncomingCallScreen(_currentCall!, navigatorKey!.currentContext!);
        }
      }

      _currentCall?.onLocalStreamReceived = (localStream) {
        localMediaStream = localStream;
      };

      _currentCall?.onRemoteStreamReceived = (session, userId, stream) {
        remoteStreams[userId] = stream;
      };

      _currentCall?.onRemoteStreamRemoved = (session, userId, stream) {
        remoteStreams.remove(userId);
      };

      _currentCall?.onReceiveHungUpFromUser =
          (session, userId, [userInfo]) async {
        print("CALL_MANAGER: onReceiveHungUpFromUser");
        /* systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_HUNGUP";
        print("-------------BROADCASTING CALL HUNGUP EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
        broadcastSystemMessage("CALL_HUNGUP");
        await player.release();
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      };
    };

    _callClient!.onSessionClosed = (callSession) async {
      if (_currentCall != null &&
          _currentCall!.sessionId == callSession.sessionId) {
        _currentCall = null;
        localMediaStream?.getTracks().forEach((track) async {
          await track.stop();
        });
        await localMediaStream?.dispose();
        localMediaStream = null;

        remoteStreams.forEach((key, value) async {
          await value.dispose();
        });

        remoteStreams.clear();
        CallKitManager.instance.processCallFinished(callSession.sessionId);
        if (GoRouter.of(navigatorKey!.currentContext!)!.location ==
            RoutesName.incomingVideoCall) {
          GoRouter.of(navigatorKey!.currentContext!).pop();
        }
      }
    };
  }

  void startNewCall(BuildContext context, int callType, int patientCubeId,
      MyAppointment appointment, String? businessId, String? moduleId) async {
    this.context = context;
    Set<int> opponents = {patientCubeId};
    if (opponents.isEmpty) return;
    if (!kIsWeb) {
      if (Platform.isIOS) {
        await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
            preferSpeakerOutput: true);
      }
    }
    P2PSession callSession =
        await _callClient!.createCallSession(callType, opponents);
    _currentCall = callSession;
    // storing it in authbloc
    context.read<AuthBloc>().p2pSession = callSession;
    context.read<AuthBloc>().resourceId = resourceId;
    context.read<AuthBloc>().selfCubeId = selfCubeId;

    String callerName;
    if (appointment.doctorMap?["firstname"] != null) {
      callerName =
          "${appointment.doctorMap?["firstname"]} ${appointment.doctorMap?["lastname"]}";
    } else {
      callerName = "";
    }

    await _sendStartCallSignalForOffliners(_currentCall!, callerName);
    GoRouter.of(context)
        .pushNamed(RoutesName.videoCallDoctorRoute, pathParameters: {
      'businessId': encrypt(text: businessId ?? '', context: context),
    }, queryParameters: {
      'moduleId': moduleId,
    }, extra: {
      // 'callSession': callSession,
      'appointment': appointment,
    });
  }

  void _showIncomingCallScreen(P2PSession callSession, BuildContext context) {
    if (navigatorKey!.currentContext != null) {
      GoRouter.of(navigatorKey!.currentContext!).pushNamed(
          RoutesName.incomingVideoCall,
          extra: Map<String, String>.from(
              {"resourceId": resourceId, "selfCubeId": selfCubeId}));
    }
  }

  void acceptCall(
    String sessionId,
    Map<String, String> userInfo,
    bool fromCallkit,
  ) async {
    await player.release();
    log('acceptCall, from callKit: $fromCallkit', TAG);
    //ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);
    if (_currentCall != null) {
      if (context != null) {
        // if (AppLifecycleState.resumed !=
        //     WidgetsBinding.instance.lifecycleState) {
        await _currentCall?.acceptCall();
        // }
        MyAppointment appointment = MyAppointment(
            id: userInfo['appointment_id'], doctor: userInfo['doctor_id']);
        log(userInfo.toString(), "User Info from AcceptCall");
        if (!fromCallkit) {
          await ConnectycubeFlutterCallKit.reportCallAccepted(
              sessionId: sessionId);
        }
        broadcastSystemMessage("CALL_ACCEPTED");
        navigatorKey!.currentContext!.read<AuthBloc>().p2pSession =
            _currentCall;
        GoRouter.of(navigatorKey!.currentContext!).pushReplacementNamed(
          RoutesName.videoCallPatientRoute,
          extra: Map<String, Object>.from({
            // 'currentCall': _currentCall!,
            'appointment': appointment,
          }),
        );

        /*  systemMessage.recipientId = int.tryParse(selfCubeId!);
        systemMessage.properties["resourceId"] = resourceId;
        systemMessage.properties["action"] = "CALL_ACCEPTED";
        print("-------------BROADCASTING ACCEPT CALL EVENT-----------");
        systemMessagesManager?.sendSystemMessage(systemMessage); */
      }
      if (!kIsWeb) {
        if (Platform.isIOS) {
          await Helper.setAppleAudioIOMode(AppleAudioIOMode.localAndRemote,
              preferSpeakerOutput: true);
        }
      }
    }
  }

  void reject(String sessionId, bool fromCallkit) {
    if (_currentCall != null) {
      player.release();
      broadcastSystemMessage("CALL_REJECTED");
      if (fromCallkit) {
        ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
      } else {
        CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      }

      _currentCall!.reject();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  void hungUp() {
    if (_currentCall != null) {
      player.release();
      CallKitManager.instance.processCallFinished(_currentCall!.sessionId);
      _currentCall!.hungUp();
      _sendEndCallSignalForOffliners(_currentCall, null);
    }
  }

  CreateEventParams _getCallEventParameters(
      P2PSession currentCall, String? callerName) {
    /* String? callerName = users
        .where((cubeUser) => cubeUser.id == currentCall.callerId)
        .first
        .fullName; */
    CreateEventParams params = CreateEventParams();
    params.parameters = {
      'message':
          "Incoming ${currentCall.callType == CallType.VIDEO_CALL ? "Video" : "Audio"} call",
      PARAM_CALL_TYPE: currentCall.callType,
      PARAM_SESSION_ID: currentCall.sessionId,
      PARAM_CALLER_ID: currentCall.callerId,
      PARAM_CALL_OPPONENTS: currentCall.opponentsIds.join(','),
      PARAM_CALLER_NAME: callerName ?? "Doctor",
    };

    params.notificationType = NotificationType.PUSH;
    params.environment = CubeEnvironment.DEVELOPMENT;
    //kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
    params.usersIds = currentCall.opponentsIds.toList();

    return params;
  }

  Future<void> _sendStartCallSignalForOffliners(
      P2PSession currentCall, String callerName) async {
    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_START_CALL;
    params.parameters[PARAM_IOS_VOIP] = 1;
    params.parameters[PARAM_EXPIRATION] = 0;
    params.parameters['ios_push_type'] = 'voip';

    await createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created: $cubeEvent");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _sendEndCallSignalForOffliners(
      P2PSession? currentCall, String? callerName) {
    if (currentCall == null) return;

    CubeUser? currentUser = CubeChatConnection.instance.currentUser;
    if (currentUser == null || currentUser.id != currentCall.callerId) return;

    CreateEventParams params = _getCallEventParameters(currentCall, callerName);
    params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_END_CALL;

    createEvent(params.getEventForRequest()).then((cubeEvent) {
      log("Event for offliners created");
    }).catchError((error) {
      log("ERROR occurs during create event");
    });
  }

  void _initCallKit() {
    CallKitManager.instance.init(
      onCallAccepted: (uuid) {
        acceptCall(uuid, _currentCall?.cubeSdp.userInfo ?? {}, true);
      },
      onCallEnded: (uuid) {
        reject(uuid, true);
      },
      onMuteCall: (mute, uuid) {
        onMicMuted?.call(mute, uuid);
      },
    );
  }

  void _initChatConnectionStateListener(BuildContext context) {
    CubeChatConnection.instance.connectionStateStream.listen((state) {
      if (CubeChatConnectionState.Ready == state) {
        _initCalls(context);
      }
    });
  }

  Future<String> _getCallState(String sessionId) async {
    if (Platform.isAndroid || Platform.isIOS) {
      var callState =
          await ConnectycubeFlutterCallKit.getCallState(sessionId: sessionId);

      log("CONECTICUBE CALL STATE: $callState");
      return callState;
    } else {
      return Future.value(CallState.UNKNOWN);
    }
  }

  void muteCall(String sessionId, bool mute) {
    CallKitManager.instance.muteCall(sessionId, mute);
  }

  Future<bool> _onBackPressed(BuildContext context) {
    return Future.value(false);
  }

  void broadcastSystemMessage(action) {
    systemMessage.recipientId = int.tryParse(selfCubeId!);
    systemMessage.properties["resourceId"] = resourceId;
    systemMessage.properties["action"] = action;
    print("BROADCASTING: $action EVENT");
    systemMessagesManager?.sendSystemMessage(systemMessage);
  }
}
Share edited Mar 3 at 11:29 yash gupta asked Mar 3 at 11:13 yash guptayash gupta 871 silver badge8 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

i've checked on p2p_call_sample and answer button works as expected, app opens and call is accepted. In logcat appears NotificationReceiver onReceive action: action_call_accept

I'd suggest you to compare your code with sample. And look at official docs.

发布者:admin,转转请注明出处:http://www.yc00.com/questions/1745097391a4611062.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信