handlePushNotification function

Future<void> handlePushNotification(
  1. RemoteMessage message
)

Initializes the FlutterCallkitIncoming and displays an incoming call notification, if the provided message is about a call.

Must be a top level function, as intended to be used as a Firebase Cloud Messaging notification background handler.

Implementation

@pragma('vm:entry-point')
Future<void> handlePushNotification(RemoteMessage message) async {
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);

  Log.debug('handlePushNotification($message)', 'main');

  final String? tag = message.notification?.android?.tag;
  final bool isCall =
      tag?.endsWith('_call') == true || tag?.endsWith('-call') == true;

  // Since tags are only working under Android, thus this code is related to
  // Android platform only - iOS doesn't execute that.
  if (isCall && message.data['chatId'] != null) {
    final ChatId chatId = message.data['chatId'];

    SharedPreferences? prefs;
    CredentialsDriftProvider? credentialsProvider;
    AccountDriftProvider? accountProvider;
    GraphQlProvider? provider;
    StreamSubscription? subscription;

    try {
      FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {
        switch (event!.event) {
          case Event.actionCallAccept:
            await prefs?.setString('answeredCall', chatId.val);
            break;

          case Event.actionCallDecline:
            await provider?.declineChatCall(chatId);
            break;

          case Event.actionCallEnded:
          case Event.actionCallTimeout:
            subscription?.cancel();
            provider?.disconnect();
            break;

          case Event.actionCallCallback:
            // TODO: Handle.
            break;

          default:
            break;
        }
      });

      // TODO: Use stored in [ApplicationSettings] language here.
      await L10n.init();

      await FlutterCallkitIncoming.showCallkitIncoming(
        CallKitParams(
          id: chatId.val,
          nameCaller: message.notification?.title ?? 'gapopa',
          appName: 'Gapopa',
          avatar: '', // TODO: Add avatar to FCM notifications.
          handle: chatId.val,
          type: 0,
          textAccept: 'btn_accept'.l10n,
          textDecline: 'btn_decline'.l10n,
          duration: 30000,
          extra: {'chatId': chatId.val},
          headers: {'platform': 'flutter'},
          android: AndroidParams(
            isCustomNotification: true,
            isShowLogo: false,
            ringtonePath: 'ringtone',
            backgroundColor: '#0955fa',
            backgroundUrl: '', // TODO: Add avatar to FCM notifications.
            actionColor: '#4CAF50',
            textColor: '#ffffff',
            incomingCallNotificationChannelName: 'label_incoming_call'.l10n,
            missedCallNotificationChannelName: 'label_chat_call_missed'.l10n,
            isShowCallID: true,
            isShowFullLockedScreen: true,
          ),
        ),
      );

      await Config.init();
      final common = CommonDriftProvider.from(CommonDatabase());
      credentialsProvider = CredentialsDriftProvider(common);
      accountProvider = AccountDriftProvider(common);

      await credentialsProvider.init();
      await accountProvider.init();

      final UserId? userId = accountProvider.userId;
      final Credentials? credentials = userId != null
          ? await credentialsProvider.read(userId)
          : null;

      if (credentials != null) {
        provider = GraphQlProvider();
        provider.token = credentials.access.secret;
        provider.reconnect();

        subscription = provider.chatEvents(chatId, null, () => null).listen((
          e,
        ) async {
          final events = ChatEvents$Subscription.fromJson(e.data!).chatEvents;
          if (events.$$typename == 'Chat') {
            final mixin = events as ChatEvents$Subscription$ChatEvents$Chat;
            final call = mixin.ongoingCall;

            if (call != null) {
              if (call.members.any((e) => e.user.id == credentials.userId)) {
                await FlutterCallkitIncoming.endCall(chatId.val.base62ToUuid());
              }
            }
          } else if (events.$$typename == 'ChatEventsVersioned') {
            final mixin =
                events
                    as ChatEvents$Subscription$ChatEvents$ChatEventsVersioned;

            for (var e in mixin.events) {
              if (e.$$typename == 'EventChatCallFinished') {
                await FlutterCallkitIncoming.endCall(chatId.val.base62ToUuid());
              } else if (e.$$typename == 'EventChatCallStarted') {
                final node =
                    e as ChatEventsVersionedMixin$Events$EventChatCallStarted;

                if (node.call.members.any(
                  (e) => e.user.id == credentials.userId,
                )) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              } else if (e.$$typename == 'EventChatCallConversationStarted') {
                final node =
                    e
                        as ChatEventsVersionedMixin$Events$EventChatCallConversationStarted;

                if (node.call.members.any(
                  (e) => e.user.id == credentials.userId,
                )) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              } else if (e.$$typename == 'EventChatCallAnswerTimeoutPassed') {
                var node =
                    e
                        as ChatEventsVersionedMixin$Events$EventChatCallAnswerTimeoutPassed;
                if (node.userId == credentials.userId) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              } else if (e.$$typename == 'EventChatCallMemberJoined') {
                var node =
                    e as ChatEventsVersionedMixin$Events$EventChatCallMemberJoined;
                if (node.user.id == credentials.userId) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              } else if (e.$$typename == 'EventChatCallMemberLeft') {
                var node =
                    e as ChatEventsVersionedMixin$Events$EventChatCallMemberLeft;
                if (node.user.id == credentials.userId) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              } else if (e.$$typename == 'EventChatCallDeclined') {
                var node =
                    e as ChatEventsVersionedMixin$Events$EventChatCallDeclined;
                if (node.user.id == credentials.userId) {
                  await FlutterCallkitIncoming.endCall(
                    chatId.val.base62ToUuid(),
                  );
                }
              }
            }
          }
        });

        prefs = await SharedPreferences.getInstance();
        await prefs.remove('answeredCall');

        // Ensure that we haven't already joined the call.
        final query = await provider.getChat(chatId);
        final call = query.chat?.ongoingCall;
        if (call != null) {
          if (call.members.any((e) => e.user.id == credentials.userId)) {
            await FlutterCallkitIncoming.endCall(chatId.val.base62ToUuid());
          }
        }
      }

      // Remove the incoming call notification after a reasonable amount of
      // time for a better UX.
      await Future.delayed(30.seconds);

      await FlutterCallkitIncoming.endCall(chatId.val.base62ToUuid());
    } catch (_) {
      provider?.disconnect();
      subscription?.cancel();
      await FlutterCallkitIncoming.endCall(chatId.val.base62ToUuid());
    }
  } else {
    // If message contains no notification (it's a background notification),
    // then try canceling the notifications with the provided thread, if any, or
    // otherwise a single one, if data contains a tag.
    if (message.notification == null ||
        (message.notification?.title == 'Canceled' &&
            message.notification?.body == null)) {
      final String? tag = message.data['tag'];
      final String? thread = message.data['thread'];

      if (PlatformUtils.isAndroid) {
        final FlutterLocalNotificationsPlugin plugin =
            FlutterLocalNotificationsPlugin();

        Future.delayed(const Duration(milliseconds: 16), () async {
          final notifications = await plugin.getActiveNotifications();

          for (var e in notifications) {
            if (e.tag?.contains(thread ?? tag ?? '.....') == true) {
              plugin.cancel(e.id ?? 0, tag: e.tag);
            }
          }
        });
      } else if (PlatformUtils.isIOS) {
        if (thread != null) {
          await IosUtils.cancelNotificationsContaining(thread);
        } else if (tag != null) {
          await IosUtils.cancelNotification(tag);
        }
      }
    }

    // If payload contains a `ChatId` in it, then try sending a single
    // [GraphQlProvider.chatItems] query to mark the chat as delivered.
    //
    // Note, that on iOS this behaviour is done via separate Notification
    // Service Extension, as this code isn't guaranteed to be invoked at all,
    // especially for visual notifications.
    if (PlatformUtils.isAndroid) {
      final String? chatId = message.data['thread'] ?? message.data['chatId'];

      if (chatId != null) {
        await Config.init();

        final common = CommonDriftProvider.from(CommonDatabase());
        final credentialsProvider = CredentialsDriftProvider(common);
        final accountProvider = AccountDriftProvider(common);

        await credentialsProvider.init();
        await accountProvider.init();

        final UserId? userId = accountProvider.userId;
        final Credentials? credentials = userId != null
            ? await credentialsProvider.read(userId)
            : null;

        if (credentials != null) {
          final provider = GraphQlProvider();
          provider.token = credentials.access.secret;

          try {
            await provider.chatItems(ChatId(chatId), first: 1);
          } catch (e) {
            // No-op.
          }

          provider.disconnect();
        }
      }
    }
  }
}