Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to emit signal to QML from Java callback executed from QtActivity class started via external intents?



  • I have been searching for an answer to this for 2 days and at last I have resorted to asking on the forums. If this is already answered somewhere, I apologize for not being able to find it.

    I want my Qt app to be able to receive intents from other Android apps and send them to QML via a signal. Right now I only want to test this with simple text/plain intents. I want a user to select some text on Android, click share, select my app, then when my app opens it displays that text somewhere. So far I have learned that to register Java natives in a Java class that inherits QtActivity, you must do it from the main.cpp file otherwise it crashes.
    I am programming this from Linux using Qt 5.15.
    Here is my main.cpp file

    //main.cpp
    #include <QApplication>
    #include <QQmlApplicationEngine>
    #include <QQmlContext>
    #include "backend.h"
    
    #ifdef Q_OS_ANDROID
    //Register special Java callback functions immediately once the app loads because they must be executed if the app is started via external Android intents
    
    static void shareTextToQML(JNIEnv *env, jobject thiz, jstring text)
    {
        //This works
        qDebug() << env->GetStringUTFChars(text, nullptr);
    
        //This is not received in QML
        emit BackEnd::instance()->testSignal(env->GetStringUTFChars(text, nullptr));
    }
    
    static JNINativeMethod methods[] = {
        { "javaShareTextToQML", // const char* function name;
            "(Ljava/lang/String;)V", // const char* function signature
            (void *)shareTextToQML // function pointer
        }
    };
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/)
    {
        JNIEnv* env;
        // get the JNIEnv pointer.
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK)
            return JNI_ERR;
    
        // step 3
        // search for Java class which declares the native methods
        jclass javaClass = env->FindClass("org/sien/qfandid/ShareActivity");
        if (!javaClass)
            return JNI_ERR;
    
        // step 4
        // register our native methods
        if (env->RegisterNatives(javaClass, methods, sizeof(methods) / sizeof(methods[0])) < 0)
            return JNI_ERR;
    
        return JNI_VERSION_1_6;
    }
    #endif
    
    int main(int argc, char *argv[])
    {
        QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
        QApplication app(argc, argv);
    
        QQmlApplicationEngine engine;
    
        //This is just a shared enumeration between C++ and QML. It's not related to my problem
        BackEnd::registerRequestTypeInQML();
    
        BackEnd *backend = new BackEnd(&app);
        engine.rootContext()->setContextProperty(QLatin1String("rootBackend"), backend);
    
        const QUrl url(QStringLiteral("qrc:/main.qml"));
        QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
                         &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
        engine.load(url);
    
        return app.exec();
    }
    

    And here is my java file that receives outside intents

    //ShareActivity.java
    package org.sien.qfandid;
    
    import org.qtproject.qt5.android.bindings.QtActivity;
    import android.content.Intent;
    import java.io.File;
    import android.net.Uri;
    import android.os.Bundle;
    
    public class ShareActivity extends QtActivity
    {
        public ShareActivity() {}
    
        private static native void javaShareTextToQML(String text);
    
        public static boolean intentPending;
        public static boolean intentInitialized;
    
        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
        }
    
        @Override
        public void onNewIntent(Intent intent)
        {
            super.onNewIntent(intent);
    
            processIntent(intent);
        }
    
        private void processIntent(Intent intent)
        {
            Uri uri;
            String scheme;
            String action = intent.getAction();
            String type = intent.getType();
    
            if (Intent.ACTION_SEND.equals(action) && type != null)
            {
                if ("text/plain".equals(type))
                {
                    String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
                    if (sharedText != null)
                    
                        //This is where I call the C++ function
                        javaShareTextToQML(sharedText);
                }
            }
        }
    }
    

    Now this is part of the main.qml file where I have a connections object and I want to output the shared text from here, but it is never received.

    //main.qml
        Connections {
            target: rootBackend
            function onTestSignal(message)
            {
                //globalBackend.makeNotification("Test", message)
                console.debug("received signal")
                console.debug(message)
            }
        }
    

    And now here is how I've implemented the instance() method I use in the main.cpp file:
    Relevant parts from the backend.h file:

    //backend.h
    class BackEnd : public QObject
    {
        Q_OBJECT
    
        QML_ELEMENT
    
    public:
        explicit BackEnd(QObject *parent = nullptr);
    
        //For getting the BackEnd instance in order to access and send signals from static methods
        //E.g. native C++ methods called from Java must be static
        static BackEnd *m_instance;
    
        static BackEnd *instance()
        {
            return m_instance;
        }
    
    //etc etc
    
    signals:
        void testSignal(QString message);
    

    Instance related parts from the backend.cpp file:

    //backend.cpp
    #include "backend.h"
    
    //etc etc
    
    BackEnd *BackEnd::m_instance = nullptr;
    
    BackEnd::BackEnd(QObject *parent) : QObject(parent)
    {
        m_instance = this;
    }
    

    I'm not sure if I need to share my android manifest too, but here are some relevant parts from it

    //Android manifest
    <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:name="org.sien.qfandid.ShareActivity" android:label="-- %%INSERT_APP_NAME%% --" android:screenOrientation="unspecified" android:launchMode="singleInstance" android:taskAffinity="">
    
    //etc etc
    
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    

    I believe everything is in place as it should be. I tried to cut out as much irrelevant code as I could, but I still ended up pasting quite a lot so I apologize.
    The current state so far is: when I launch the app, move out of it, select some text, click share, then select my app it reaches as far as the qDebug() << env->GetStringUTFChars(text, nullptr); part in the main.cpp. However, when I try to execute emit BackEnd::instance()->testSignal(env->GetStringUTFChars(text, nullptr)); afterwards, the signal is not received in the little Connections object defined above in main.qml.

    From the tests I've performed so far I assume the signal doesn't reach QML because BackEnd::instance() doesn't actually return the instance where the QML interface "lives", though I may be wrong. Could anyone help me? I don't know what else to try. I've spent almost 2 days reading blogs, forums and the documentation, but there aren't many examples of what I'm trying to do. The closest I could find was this old blog post https://www.qt.io/blog/2018/01/16/sharing-files-android-ios-qt-app-part-2
    But the blog post itself doesn't show all the necessary information and the code in the github page was too complicated for me to understand how to do exactly what I want to do.

    To reiterate, all that's left to do is figure out how to send the string message to QML from the main.cpp function static void shareTextToQML(JNIEnv *env, jobject thiz, jstring text). However, I assume there's a discrepancy between the instance returned from BackEnd::instance() and the instance where the QML interface resides, but I don't know how to resolve it. Any help would be much appreciated.



  • I believe I just solved it after sleeping on it lol. I was so close I don't know how I didn't see this before.

    I followed the assumption that there's an instance discrepancy and I thought that it was caused by the fact that m_instance , a static variable, was changed every time a new BackEnd class was created (which happens very often in my app), because m_instance = this was executed in the BackEnd constructor. So I did some code reorganization:

    In the header file I moved static BackEnd *m_instance; to become a private variable and declared this public class method Q_INVOKABLE void storeQmlInstance();
    In the cpp file I removed this line m_instance = this; from the constructor, so now it looks like this

    BackEnd::BackEnd(QObject *parent) : QObject(parent)
    {
    
    }
    

    Then I implemented the method like this:

    void BackEnd::storeQmlInstance()
    {
        m_instance = this;
    }
    

    Now, most importantly, in the main.qml file I added this line Component.onCompleted: globalBackend.storeQmlInstance();. (globalBackend is the ID of the BackEnd instance defined in the main.qml)
    Now remember that the method instance() is still a public static method which simply returns m_instance.
    So now, in my main.cpp when this is executed

    static void shareTextToQML(JNIEnv *env, jobject thiz, jstring text)
    {
        emit BackEnd::instance()->testSignal(env->GetStringUTFChars(text, nullptr));
    }
    

    It is finally received in the main.qml Connections object, whose target this time is globalBackend because that's where storeQmlInstance() is called from.


Log in to reply