handlePushNotification function

  1. @pragma('vm:entry-point')
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;

  if (isCall && message.data['chatId'] != null) {
    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', message.data['chatId']);
            break;

          case Event.actionCallDecline:
            await provider?.declineChatCall(ChatId(message.data['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: message.data['chatId'],
          nameCaller: message.notification?.title ?? 'gapopa',
          appName: 'Gapopa',
          avatar: '', // TODO: Add avatar to FCM notifications.
          handle: message.data['chatId'],
          type: 0,
          textAccept: 'btn_accept'.l10n,
          textDecline: 'btn_decline'.l10n,
          duration: 30000,
          extra: {'chatId': message.data['chatId']},
          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(message.data['chatId']), null, () => null)
            .listen((e) async {
              var events = ChatEvents$Subscription.fromJson(e.data!).chatEvents;
              if (events.$$typename == 'ChatEventsVersioned') {
                var mixin =
                    events
                        as ChatEvents$Subscription$ChatEvents$ChatEventsVersioned;

                for (var e in mixin.events) {
                  if (e.$$typename == 'EventChatCallFinished') {
                    await FlutterCallkitIncoming.endCall(
                      (message.data['chatId'] as String).base62ToUuid(),
                    );
                  } else if (e.$$typename == 'EventChatCallMemberJoined') {
                    var node =
                        e
                            as ChatEventsVersionedMixin$Events$EventChatCallMemberJoined;
                    if (node.user.id == credentials.userId) {
                      await FlutterCallkitIncoming.endCall(
                        (message.data['chatId'] as String).base62ToUuid(),
                      );
                    }
                  } else if (e.$$typename == 'EventChatCallDeclined') {
                    var node =
                        e as ChatEventsVersionedMixin$Events$EventChatCallDeclined;
                    if (node.user.id == credentials.userId) {
                      await FlutterCallkitIncoming.endCall(
                        (message.data['chatId'] as String).base62ToUuid(),
                      );
                    }
                  }
                }
              }
            });

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

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

      await FlutterCallkitIncoming.endCall(
        (message.data['chatId'] as String).base62ToUuid(),
      );
    } catch (_) {
      provider?.disconnect();
      subscription?.cancel();
      await FlutterCallkitIncoming.endCall(
        (message.data['chatId'] as String).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();
        }
      }
    }
  }
}