How to execute a foreground service in Qt Android application that is part of a Xamarin Android app
-
I have a bit of a unique situation. I have a front end QML/Qt UI and a backend application written in C# that I'm developing to replace old software. I was previously using Android tablets with an all in one Xamarin UI. This new combo will replace that application. Problem is on the old Android tablets to put the new software on there we need some way to run our C# backend without having to duplicate it in another language or build it into the UI.
I stripped out everything of the Xamarin UI so now its just a blank UI with a foreground service within the project. I tested out executing/starting the foreground service within the Xamarin activity to make sure it works. The service will continually make a beeping noise every second (as a proof of concept that I can get this to work).
[Service(Exported = true, Name = "com.backend.RUN_BACKEND")] public class ForegroundBackendService : Service //This is the function that gets called currently when the intent //comes in. I create a notification, start foreground service, //launch the process/task, and then return a sticky result. public override StartCommandResult OnStartCommand(Intent intent, StartCommandFlags flags, int startId)
The exported setting should mean that this service will be available to any application on the Android tablet. Another application should be able to use the "com.backend.RUN_BACKEND" name to execute this foreground service.
The Xamarin project/app is named TestProject. And this is running on Android 8.1 minimum / 8.1 target (Qt UI is also running there).
The Qt application is built with Qt 6.5.0 but it seems most of the examples are a bit dated and the libraries like QtAndroid have been replaced.
This code below seems to be close to what I'm looking for but QAndroidApplication::androidActivity().object isn't available anymore it seems. So I'm trying to figure out what the new syntax to execute the service.
QAndroidIntent serviceIntent(QAndroidApplication::androidActivity().object(), "org/qtproject/example/qtandroidservice/QtAndroidService"); QJniObject result = QAndroidApplication::androidActivity().callObjectMethod( "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;", serviceIntent.handle().object());
What would the syntax/code be to execute my ForegroundBackgroundService named "com.backend.RUN_BACKEND" from the Qt UI?
Thanks ahead of time I really appreciate the help!
I posted this question to StackOverflow also so the link to that question is here if you want to answer it there. Otherwise answer here and I'll update my question there too once I can get this issue solved.
How to execute a foreground service in Xamarin Android app from a Qt Android application
-
So I found a link that was sort of helpful.
How do I access Android activity in QT6
That links to this page which I had already come across but I'm now taking a deeper look at it.
QNativeInterface::QAndroidApplication
So now I am trying to figure out what the equivalent syntax is. I'm new to Qt/Android/Android services so maybe the answer is obvious and I'm just missing it.
Whats confusing me is that QAndroidApplication::androidActivity() has members object() and callObjectMethod(). Here is a list of what is available in QNativeInterface::QAndroidApplication. Its not the same so I'm trying to figure out how to write an equivalent piece of code.
context() : int hideSplashScreen(int) isActivityContext() : bool runOnAndroidMainThread(const std::function<QVariant ()> &, const QDeadlineTimer) : QFuture<QVariant> sdkVersion() : int
Based on this information can anyone list out an equivalent bit of code using the new QNativeInterface::QAndroidApplication?
I'm still digging into it so hopefully I can figure it out.
-
I just came across another link that might be of help to figure this out.
I am trying to figure out what this means now and how to write equivalent code.
-
Ok so I had overlooked some comments on that QtBug Report link I posted before. I found some new code.
QJniObject jClass = QJniObject("poc/dialogue/Hello", " (Landroid/app/Activity;)V", QtAndroidPrivate::activity()); jClass.callMethod<void ("showDialogue"); //This seems to solve part of the problem QAndroidIntent serviceIntent = QAndroidIntent(QtAndroidPrivate::activity(), "com.example.MyService"); //Now I need some sort of piece of code that is equivalent to this QJniObject result = QAndroidApplication::androidActivity().callObjectMethod( "startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;", serviceIntent.handle().object());
I also need to execute functions from a Java SDK so the first couple lines are still useful.
So it seems what I need now is to find out how to execute the QAndroidIntent serviceIntent.
I do not need to bind to the service I just need to execute it. The service will talk to the frontend over a websocket.
-
I have code now that isn't highlighted so I think I'm headed in the right direction.
Does this look right?
QAndroidIntent serviceIntent = QAndroidIntent(QtAndroidPrivate::activity(), "com.example.MyService"); QJniObject startService = QJniObject(); QJniObject intentResult = startService.callObjectMethod("startService", "(Landroid/content/Intent;)Landroid/content/ComponentName;", serviceIntent.handle().object());
Is there anything obviously wrong with the above code. I'm going to test it now so I guess I'll find out.
-
I am honing in on what I think is a solution.
//I have this line of code but it produces an error QAndroidIntent serviceIntent = QAndroidIntent(QtAndroidPrivate::activity(), "SOB.Bacon.com.backend.RunBackend"); //JNI DETECTED ERROR IN APPLICATION: illegal class name //SOB.Bacon/com.backend.RunBackend' F zygote : runtime.cc:550] //(should be of //the form 'package/Class', [Lpackage/Class;' or '[[B') F zygote : runtime.cc:550] //in call to FindClass
I have tried all interations of "SOB.Bacon/com.backend.RunBackend" such as "SOB/Bacon/com/backend/RunBackend" and "SOB.Bacon.com.backend.RunBackend". Nothing seems to work.
I know for a fact that this service works. I created a second bare bones blank Xamarin project and it does one simple thing the project loads up and using this code it launches the foreground service of the other application. And bam the service starts beeping and I know it worked. It launches it by using ONLY the package name "SOB.Bacon" and the class name "com.backend.RunBackend" thats it. So I should be able to do the exact same thing from Qt.
const string REMOTE_SERVICE_COMPONENT_NAME = "com.backend.RunBackend"; // This is the name of the service, according the value of ServiceAttribute.Name const string REMOTE_SERVICE_PACKAGE_NAME = "SOB.Bacon"; // Provide the package name and the name of the service with a ComponentName object. ComponentName cn = new ComponentName(REMOTE_SERVICE_PACKAGE_NAME, REMOTE_SERVICE_COMPONENT_NAME); Intent serviceToStart = new Intent(); serviceToStart.SetComponent(cn); StartForegroundService(serviceToStart);
I do not understand why Qt can't seem to find this package/service. It does exist so hopefully I'm just missing something obvious.
Can anyone shed any light on this? I have been working on this for two days and have just about exhausted the internet. There are few examples of this stuff for Qt6 most is Qt5 stuff otherwise I'm sure I would've had this solved in a hour.
I have to be really close. All I want to do is replicate in Qt6 exactly what that little snippet of Xamarin code does above thats it.
So please if you know the answer post I'm going in circles trying to get this working.
-
Ok I stumbled onto a new post that is encouraging and seems VERY relevant.
I found a new link that is helpful QtAndroid grant usb permission in android with Qt6
This post also confirms that with Qt6 all this stuff is difficult as its all new and not documented with a lot of examples. So it'll take time for this to change. I'm hoping this forum post and questions will help others once I get this solved.
I'm pasting in the code from that response here. Look at this code it seems so close to what I want to do.
All I need to do is make an intent and hit the startService method and pass the intent. That should do the trick and I do not need to bind or exchange data. The project name is SOB.Bacon and the service is named com.backend.RUN_BACKEND.
Get the context and usb manager object : QJniObject context = QtAndroidPrivate::context(); if (context.isValid()) { QJniObject usbManager = context.callObjectMethod( "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;", QJniObject::getStaticObjectField( "android/content/Context", "USB_SERVICE", "Ljava/lang/String;" ).object<jstring>() ); Iterate on the deviceList obtained from usbManager : QJniObject deviceList = usbManager.callObjectMethod( "getDeviceList", "()Ljava/util/HashMap;" ); //Retrieve Java objects to be able to iterate deviceList QJniEnvironment env; jobject jMap = deviceList.object(); jclass mapClass = env->GetObjectClass(jMap); jmethodID entrySet = env->GetMethodID(mapClass, "entrySet", "()Ljava/util/Set;"); jobject jEntrySet = env->CallObjectMethod(jMap, entrySet); jclass setClass = env->FindClass("java/util/Set"); jmethodID iterator = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;"); jobject jIterator = env->CallObjectMethod(jEntrySet, iterator); jclass iteratorClass = env->FindClass("java/util/Iterator"); jmethodID hasNext = env->GetMethodID(iteratorClass, "hasNext", "()Z"); jmethodID next = env->GetMethodID(iteratorClass, "next", "()Ljava/lang/Object;"); jclass entryClass = env->FindClass("java/util/Map$Entry"); jmethodID getKey = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;"); jmethodID getValue = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;"); Get your device object and obtain all information needed (if you want to print for debug or something else) while (env->CallBooleanMethod(jIterator, hasNext)) { jobject jEntry = env->CallObjectMethod(jIterator, next); jstring jDeviceName = static_cast<jstring>(env->CallObjectMethod(jEntry, getKey)); jobject jDevice = env->CallObjectMethod(jEntry, getValue); QJniObject device(jDevice); jint vendorId = device.callMethod<jint>("getVendorId", "()I"); jint productId = device.callMethod<jint>("getProductId", "()I"); QString manufacturer = (device.callMethod<jstring>("getManufacturerName", "()Ljava/lang/String;")).toString(); QString productname = (device.callMethod<jstring>("getProductName", "()Ljava/lang/String;")).toString(); And this is how you request permission to a specific device jstring permissionString = env->NewStringUTF("org.qtproject.socomate.USB_PERMISSION"); QJniObject intent("android/content/Intent"); jclass stringClass = env->FindClass("java/lang/String"); jmethodID constructor = env->GetMethodID(stringClass, "<init>", "(Ljava/lang/String;)V"); jobject stringObject = env->NewObject(stringClass, constructor, permissionString); intent.callMethod<void>("<init>", "(Ljava/lang/String;)V", stringObject); QJniObject permissionIntent("android/app/PendingIntent"); jclass pendingIntentClass = env->FindClass("android/app/PendingIntent"); jmethodID getBroadcastMethod = env->GetStaticMethodID(pendingIntentClass, "getBroadcast", "(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;"); permissionIntent = permissionIntent.callStaticObjectMethod(pendingIntentClass, getBroadcastMethod, context.object(), 0, intent.object(), 0); usbManager.callMethod<void>( "requestPermission", "(Landroid/hardware/usb/UsbDevice;Landroid/app/PendingIntent;)V", device.object(), permissionIntent.object() );
-
I really think I identified the actual issue now so I need help essentially getting my Xamarin service added/linked to the Qt project. I am unsure how to do this.
I followed the tutorial here and I have tried both starting up the function in the service and binding to it and neither work.
I am pretty convinced that the problem is that in the example they created a class QtAndroidService that extends QtService. In my case I just extended Service as QtService isn't available in Xamarin/C#. The example says this should work.
Here is the thing though. In the example it executes the function for example bycalling the method startQtAndroidService located at "org/qtproject/example/qtandroidservice/QtAndroidService".
I looked at my current Qt project and its directory starts off the exact same "org.qtproject.example.myappname". I guess I must have just built the UI off an example and never changed it.
So I think this means that somehow they put the library file QtAndroidService within the Qt project.
I'm assuming its a .so file because when I currently run it I'll get back an error saying "Didn't find class "SOB.Bacon.com.backend.RunBackend.ForegroundBackendService" on path: DexPathList[[],nativeLibraryDirectories=[/system/lib, /system/vendor/lib]]". The class is still named ForegroundBackendService but its not a foreground service anymore I followed the example exactly just kept the same name I'll change it.
If I look in the /system/lib folder on the Android device it is filled with files with lib in the name ending in .so.
If I look in my SOB.Bacon project install directory on the Android device there is a lib folder. I do not see a library with the same name as my service though, the .so files that are in there seem to have nothing to do with anything other than what Xamarin needs.
So first off I do not know how to get Xamarin to generate a .so file. It will make a .dll file but that is for use by other Xamarin applications.
So I have two routes/questions:
1. Based on that example how do I link/point Qt to look for the service in a different location on the Android tablet, right now it seems to only be looking in /system/lib or /system/vendor/lib.
2. If a .so file is required, how was the other xamarin application able to execute the service, and how was ADB able to execute the service? In which case how do I make Qt execute a service that is NOT part of its own package?
Thanks a lot!