Skip to content

Qt Allstack II – Adding Firebase

A couple of weeks ago, we guided you through setting up a chat application and server in our first blog of this series. This is the second and final blog of this Qt Allstack series.

Firebase Messaging

Now that we have a functional chat application, it’s time to add real world features, like push notifications. Firebase Cloud Messaging allows you to send push notifications to your users while your app is not running and integrates with APNs (Apple Push Notification services). This way, you only care about one API and can have push notifications on both Android and iOS.

It is important to note here that, even though you can choose not to use Cutelyst on your backend, you still need to add Firebase support on your mobile application. It needs to link to the Firebase library so it can retrieve an unique device token. Once you have the Firebase token, you can send push notifications using any kind of server. Since the idea is to use Qt on all stacks, we will cover how to do so in Cutelyst.

Back in the ChatAppBack project, we need to fetch and link to FirebaseAdminQt. Since Google doesn’t provide a Qt/C++ FirebaseAdminSDK, I have implemented one that supports some of its features.

CMake can fetch, compile and link it for you, to do so open the main CMakeLists.txt and add:

include(FetchContent)

FetchContent_Declare(
  FirebaseAdminQt
  GIT_REPOSITORY https://github.com/cutelyst/FirebaseAdminQt.git
  GIT_TAG        a9db10cdb3c1d6d68d37630fea9abcac8e640219
)
FetchContent_MakeAvailable(FirebaseAdminQt)

Then, on src/CMakeLists.txt., make sure it links to:

FirebaseAdminQt5::Core

On root.h, we will add two new methods and create a pointer to the class that can send push notifications:

class FirebaseAdminMessaging; // Forward declare the pointer type
class Root : public Controller
{
public:
    void sendPushNotifications(const QString &message);
    void sendPushNotification(const QString &deviceToken, const QString &message);

private:
    FirebaseAdminMessaging *m_fbAdminMsg;
};

We now need to filter the messages to find out whether we should send a notification to a user. We will add two methods: one to filter the message and query the user’s firebase_token and another to send the message to Firebase servers to be pushed to the mobile phone:

void Root::sendPushNotifications(const QString &message)
{
    QStringList users;
    QRegularExpression re("@([a-zA-Z0-9_]{3,})");
    auto globalMatch = re.globalMatch(message);
    while (globalMatch.hasNext()) {
        QRegularExpressionMatch match = globalMatch.next();
        users << match.captured(1);
    }
    users.removeDuplicates();

    for (const auto &user : qAsConst(users)) {
        APool::database().exec(u"SELECT data->>'firebase_token' FROM users WHERE nick=$1",
                               {
                                   user,
                               }, [=] (AResult &result) {
            auto firstRow = result.begin();
            if (!result.error()) {
                if (result.size() && !result[0][0].toString().isEmpty()) {
                    sendPushNotification(result[0][0].toString(), message);
                }
            } else {
                qWarning() << "Failed to get firebase_token for user" << user << result.errorString();
            }
        }, this);
    }
}
void Root::sendPushNotification(const QString &deviceToken, const QString &message)
{
    FirebaseMessage msg;
    msg.setToken(deviceToken);
    QString title = "ChatApp - New Message";
    msg.setNotification(title, message);
    FirebaseAndroidNotification android;
    android.setTitle(title);
    android.setBody(message);
    msg.setAndroid(android);
    FirebaseAdminReply *reply = m_fbAdminMsg->send(msg);
    connect(reply, &FirebaseAdminReply::finished, this, [=] {
        reply->deleteLater();
        if (reply->error()) {
            qDebug() << "FIREBASE error" << reply->errorCode() << reply->errorMessage();
        } else {
            qDebug() << "FIREBASE success" << reply->messageId();
        }
    });
}

Then, at the root.cpp file on messages_POST, we add the following line when we’ve successfully retrieved our message id:

sendPushNotifications(msg);

Finally, we need to initialize Firebase Admin by adding the following code at the postFork() function inside root.cpp:

    auto fbApp = new FirebaseAdmin(this);
    fbApp->setAccountCredentialData(R"V0G0N(
<REPLACE WITH SERVICE ACCOUNT JSON>
)V0G0N");
    fbApp->getAccessToken();
    auto messaging = new FirebaseAdminMessaging(fbApp);
    messaging->setApiKey("<REPLACE SERVER API KEY>");
    
    return true;

To fill both fields, we now need to create a Firebase project. So, please open the Firebase console. The FCM documentation can be found here. If you’d like more information on Firebase projects, click here.

On the Firebase website, click on “Create a project” or “Add project” (if you already have one). Give it a name, like ChatApp. In the next step, Google Analytics is not required and for simplicity, disable it. It will then create your new project and take you to the project’s dashboard.

Select “Project Settings” on the Firebase dashboard:

Choose the “Service Accounts” tab. There, we will have Firebase Admin SDK information. Click “Generate new private key.” This will download a JSON file containing the server credentials. You can either add code to read the file or copy its contents and replace the placeholder that we left on root.cpp:

FirebaseAdmin::setAccountCredentialData();

Now we select the “Cloud Messaging” tab. In “Project Credentials”, copy the token of the “Server Key” key, and replace the placeholder that we left on root.cpp:

FirebaseAdminMessaging::setApiKey();

Let’s also create a generic method to send the push notifications on root.cpp:

void Root::sendPushNotification(const QString &deviceToken, const QString &message)
{
    FirebaseMessage msg;
    msg.setToken(deviceToken);
    QString title = "ChatApp - New Message";
    msg.setNotification(title, message);
    FirebaseAndroidNotification android;
    android.setTitle(title);
    android.setBody(message);
    msg.setAndroid(android);
    FirebaseAdminReply *reply = m_fbAdminMsg->send(msg);
    connect(reply, &FirebaseAdminReply::finished, this, [=] {
        reply->deleteLater();
        if (reply->error()) {
            qDebug() << "FIREBASE error" << reply->errorCode() << reply->errorMessage();
        } else {
            qDebug() << "FIREBASE success" << reply->messageId();
        }
    });
}

Let’s switch back to our QML app and prepare ChatApp for Android. We will ask Qt Creator to create the Android templates. To do so, select “Projects” → select “Android Qt 5.15.2” kit → then, in “Build Android APK”, choose “Create Templates”. On the wizard, select to copy the gradle files checkbox, too:

Once the templates are created, you will have the Android manifest open in an editor. I recommend avoiding using that editor, as it usually reformats the XML. For now, just change the “Package Name” to “com.kdab.chatapp”. This is the ID of your APK that will eventually be published in the Google Play Store. We’ll need it on the Firebase console.

On the dashboard, there will be an Android icon with the information to “add an app to get started”. It will now show a page to register your app. I’ll put “com.kdab.chatapp” under the Android package name and then click on “Register App”, (a hash will be appended to your project id).

The next step will generate a “google-services.json” that we will need to have on our app. Download it and have it stored inside the android folder we created earlier on the app. Go to ChatApp.pro and add the following line:

DISTFILES += android/google-services.json

We will not cover the step-by-step for getting APNs to work, as that is well-covered on the web. On this Firebase Console page you can also add your iOS app information and it will provide fields for adding the your APNs certificates.

Add the QtAndroidExtras library to the top of the ChatApp.pro file:

android: {
    QT += androidextras
}

Add the following to the gradle.properties:

#org.gradle.caching=true

android.useAndroidX=true
systemProp.firebase_cpp_sdk.dir=<path_to>/firebase_cpp_sdk

This is required so that the C++ Firebase library can initialize and create a new unique token to identify your device, for privacy reasons you are not allowed to obtain any phone identification.

The next step, “Add Firebase SDK”, should be ignored because it doesn’t cover Qt. So click “Continue to Console”.

It’s time to finish and integrate Firebase C++ into our code. To start, download the Firebase C++ SDK from here.  You will have a ZIP file, which you should extract beside your ChatApp project directory.

Notice that this isn’t the latest version, but rather an older version that works. The latest version has issues calling the Java classes. This version, although old, still works without issues. On iOS we will need this very same ZIP file, but will need to manually download the Firebase iOS SDK. It usually offers adding it with Cocoa PODS, but QMake has no support for it. Basically, on iOS, the C++ SDK will talk to that of the iOS. It took me a long time to figure this out, but at least our C++ code is left untouched.

Firebase C++ SDK has one caveat, though: when a request is made or when a callback method is called, they are not on Qt’s main GUI thread. To make integration easier, I created another Qt Wrapper, but the code is not state-of-the-art. So, we must connect to all its signals using Qt::QueuedConnection.

Now we’ll clone https://github.com/cutelyst/firebase-qt inside our ChatApp and add it to the ChatApp.pro build:

android|ios {
    SOURCES += \
        firebase-qt/src/firebaseqtapp.cpp \
        firebase-qt/src/firebaseqtmessaging.cpp \
        firebase-qt/src/firebaseqtabstractmodule.cpp

    HEADERS += \
        firebase-qt/src/firebaseqtabstractmodule.h \
        firebase-qt/src/firebaseqtapp.h \
        firebase-qt/src/firebaseqtapp_p.h \
        firebase-qt/src/firebaseqtmessaging.h
}

Firebase Qt also has a class for phone authentication, which uses SMS to validate the user’s mobile phone and can easily be extended to support the other authentication methods supported by the Firebase Authentication module, which we won’t cover here.

Still on ChatApp.pro, we will add the required code to be able to work on both 32-bit and 64-bit Android. We can also check the iOS part on the git repository of this project:

# This is the path for the Firebase C++ SDK
GOOGLE_FIREBASE_SDK = $$PWD/../firebase_cpp_sdk

# This is the path for the iOS Firebase SDK
GOOGLE_IOS_FIREBASE_SDK = $$PWD/../Firebase

INCLUDEPATH += $${GOOGLE_FIREBASE_SDK}/include
DEPENDPATH += $${GOOGLE_FIREBASE_SDK}/include

contains(ANDROID_TARGET_ARCH,armeabi-v7a) {
    ANDROID_PACKAGE_SOURCE_DIR = \
        $$PWD/android

    LIBS += -L$${GOOGLE_FIREBASE_SDK}/libs/android/armeabi-v7a/c++/ -lfirebase_app -lfirebase_messaging

    PRE_TARGETDEPS += $${GOOGLE_FIREBASE_SDK}/libs/android/armeabi-v7a/c++/libfirebase_app.a
    PRE_TARGETDEPS += $${GOOGLE_FIREBASE_SDK}/libs/android/armeabi-v7a/c++/libfirebase_messaging.a
}

contains(ANDROID_TARGET_ARCH,arm64-v8a) {
    ANDROID_PACKAGE_SOURCE_DIR = \
        $$PWD/android

    LIBS += -L$${GOOGLE_FIREBASE_SDK}/libs/android/arm64-v8a/c++/ -lfirebase_app -lfirebase_messaging

    PRE_TARGETDEPS += $${GOOGLE_FIREBASE_SDK}/libs/android/arm64-v8a/c++/libfirebase_app.a
    PRE_TARGETDEPS += $${GOOGLE_FIREBASE_SDK}/libs/android/arm64-v8a/c++/libfirebase_messaging.a
}

Here, we link to firebase_app and firebase_messaging. As on iOS, Firebase C++ SDK, when running on Android requires it’s Java libraries, we get them by changing the build.gradle:

buildscript {
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.0'
        classpath 'com.google.gms:google-services:4.3.8'
    }
}

repositories {
    google()
    jcenter()
    flatDir {
        dirs "../../firebase_cpp_sdk/libs/android"
    }
}

And:

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
    implementation 'com.google.firebase:firebase-analytics:17.4.1'
    implementation 'com.google.firebase:firebase-messaging:20.1.7'
    implementation 'com.google.firebase.messaging.cpp:firebase_messaging_cpp@aar'
    implementation 'com.google.android.gms:play-services-base:16.1.0'
    implementation 'androidx.core:core:1.0.1'
}

We are now able to proceed to using it, creating a Firebase class that will serve us as the QML glue. So, let’s ask QtCreator to create a new C++ class called Firebase that inherits from QObject. The new firebase.h will look like:

class Firebase : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString token MEMBER _token NOTIFY tokenChanged)
public:
    explicit Firebase(QObject *parent = nullptr);

    void tokenReceived(const QByteArray &token);
    void messageReceived(const QMap<QString, QString> &data);

Q_SIGNALS:
    void tokenChanged();

private:
    QString _token;
};

The two methods will be called by Firebase C++ SDK, the first one when it initializes and the second one when a push notification is received but the application is running. In this case, Android does not show the popup but you still get the data. The token property will be sent to the backend; it’s needed to be able to send a notification to this device.

On firebase.cpp:

#include <QLoggingCategory>

#include "firebase-qt/src/firebaseqtapp.h"
#include "firebase-qt/src/firebaseqtmessaging.h"

Firebase::Firebase(QObject *parent) : QObject(parent)
{
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
    auto firebase = new FirebaseQtApp(this);

    auto messaging = new FirebaseQtMessaging(firebase);
    connect(messaging, &FirebaseQtMessaging::tokenReceived, this, &Firebase::tokenReceived, Qt::QueuedConnection);
    connect(messaging, &FirebaseQtMessaging::messageReceived, this, &Firebase::messageReceived, Qt::QueuedConnection);

    firebase->initialize();
#endif
}

void Firebase::tokenReceived(const QByteArray &token)
{
    qDebug() << "Got Firebase Messaging token" << token;
    _token = QString::fromLatin1(token);
    Q_EMIT tokenChanged();
}

void Firebase::messageReceived(const QMap<QString, QString> &data)
{
    qDebug() << "Got a Push Notification when the app is running" << data;
}

As we can see, we need FirebaseQtApp, which is required by all Firebase modules. Then, we connect the signals to our methods using Qt::QueuedConnection. When testing with the USB cable, we will be able to see the debug message when messageReceived is called. But we will ignore its data.

We will now export this class as a QML singleton because we need the token to be sent when the user is created. Add the following to main.cpp before you create the QGuiApplication instance:

    Firebase fb;
    qmlRegisterSingletonInstance("com.kdab", 1, 0, "Firebase", &fb);

Now, on PageUser.qml, we will import this singleton:

import com.kdab 1.0

Then, use the token on the JSON object that is sent to the server:

                xhr.send(JSON.stringify({
                                            user_id: settings.user_id,
                                            nick: nick,
                                            fullname: fullname,
                                            firebase_token: Firebase.token
                                        }));

Now, we should have Firebase notifications set up and in working order.

Watch the video in which Daniel Nicoletti takes you through the steps in this blog:

You can get the full version of the code in this blog from our GitHub.

Have fun with it!

 

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

FacebookTwitterLinkedInEmail

Categories: KDAB Blogs / KDAB on Qt / Qt / QtDevelopment / Technical

Tags: /
Leave a Reply

Your email address will not be published.