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

  // The code here should only be executed for Android devices, since other iOS
  // push notifications related code is handled either in `AppDelegate.swift` or
  // `NotificationService.swift` files due to iOS not giving guarantees about
  // calling this method.
  if (!PlatformUtils.isAndroid) {
    return;
  }

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

  final String? tag = message.data['tag'] ?? message.notification?.android?.tag;
  final String? thread = message.data['thread'];

  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) {
    final ChatId 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());
    }

    return;
  }

  if (tag != null) {
    // There may be already an local notification, thus just cancel it before
    // displaying this new one.
    final plugin = FlutterLocalNotificationsPlugin();
    try {
      await plugin.cancel(tag.asHash, tag: tag);
    } catch (_) {
      // It's ok to fail.
    }
  }

  // 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?.isEmpty != false &&
          message.notification?.body == null)) {
    await Future.delayed(Duration(milliseconds: 100), () async {
      final plugin = FlutterLocalNotificationsPlugin();

      if (thread != null) {
        for (var e in await plugin.getActiveNotifications()) {
          if (e.id != null && e.tag?.contains(thread) == true) {
            await plugin.cancel(e.id!, tag: e.tag);
          }
        }
      } else if (tag != null) {
        for (var e in await plugin.getActiveNotifications()) {
          if (e.id != null && e.tag == tag) {
            await plugin.cancel(e.id!, tag: e.tag);
          }
        }
      }
    });
  }

  // If payload contains a `ChatId` in it, then try sending a single
  // [GraphQlProvider.chatItems] query to mark the chat as delivered.
  {
    final String? chatId = 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 GraphQlProvider provider = GraphQlProvider()
          ..client.withWebSocket = false;

        provider.token = credentials.access.secret;

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

        provider.disconnect();
      }
    }
  }
}