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 theqDebug() << env->GetStringUTFChars(text, nullptr);
part in the main.cpp. However, when I try to executeemit 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 fromBackEnd::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), becausem_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 methodQ_INVOKABLE void storeQmlInstance();
In the cpp file I removed this linem_instance = this;
from the constructor, so now it looks like thisBackEnd::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 methodinstance()
is still a public static method which simply returnsm_instance
.
So now, in my main.cpp when this is executedstatic 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.