- About
- Installing QtAidl module into your Environment
- Example code
- The hidden heroes
- Licenses and Attributions
QtAidl is an experimental extension module for Qt Framework that aims to simplify the work-/data-flow in case of the need to use Android Framework APIs. While Qt framework abstracts the basic Android APIs and makes them seamless for the application developer - Qt Multimedia is a good example of that - it doesn't (and shouldn't) cover other API sets (like e.g. the one ofered by Contacts Provider in case if one would like to build the phone book application.)
It means that the effort to integrate such APIs is on the application developer's side. We could summarize the usual data flow that has to be implemented in a simplified form:
It means that whenever we would like to expose some information from Java/Kotlin APIs to QML (or send some feedback from QML back to those APIs) we need to develop the code in 4 languages:
- QML - for our presentation layer
- C++ - to expose the information from business logic layer to QtQuick engine
- C - to cross the boundaries of our Application and JVM
- Java/Kotlin to invoke the appropriate API
For many reasons the above flow might be problematic - just to name a few of them:
- Developer has to produce a lot of boilerplate and repetitive code (especially on the JNI calls level )
- The code is hard to refactor in case of the need to add new features
- The code is error prone
- JNI code doesn't provide strong typing for complex types causing the need to manually extract the data of unknown structures or perform a lot of casts
- As usually the code using Android APIs is located in the same process as UI (e.g. in Activity subclass), any exception in that area will cause the whole application to crash.
As already mentioned QtAidl aims to simplify that dataflow with the goal to replace current approach with the following one:
On the above sketch you can see the solution where Application is divided into two layers where QtAidl serves the glue between them:
- Bussines Logic Layer - enclosed in Java/Kotlin only service
- Presentation Layer - providing visual interaction with end user with QML frontends (in fact QtAidl can also be used with any Qt based C++ code).
To implement this approach, we have selected two main foundations for QtAidl module:
Qt Interface framework is built around the pattern that separates the feature from actual implementation. What's more, it allows defining or re-defining easily the APIs related to the feature and provides a way automate generation of the code that is needed to use that feature from QML or C++. It also allows creating multiple backends (implementing or abstracting access to the business logic layer) for the same feature (and discover them in runtime) which means that you can develop the application parts in separation e.g. using Simulation Backend or a Backend specific for some platform (e.g. embedded Linux) until the production backend for Android will be ready, and then you can easily make the switchover.
Android Bound Services fits well into that approach. It is using Binder IPC to implement the idiom where the business logic is enclosed in a Service (running in the separate process), while the presentation is using just a handle to that service (Local Proxy Object to be exact) to perform operations on it. As the business logic is separated from the presentation any failure in service won't make the UI process crash.
QtAidl joins those two worlds in the following way:
As you can see the need for hand written code is limited only to the:
- UI code (in our case QML)
- Bussines logic (Java or Kotlin)
The rest of the code either comes from SDKs (Qt Interface Framework Core library and Android SKD/NDK) or is auto generated.
For the needs of code generation:
QtAidl module enables easy transition between both worlds:
- It provides additional (the list of standard Qt Interface framework templates can be found here) project template -
aidl
- that:- generates AIDL files out of QFace IDL file
- generates the valid Qt Interface Framework backend that wraps Binder IPC
- generates LocalProxyObject classes using AIDL NDK Backend
- generates CMake file required to make that part of the dynamic library
- Provides gradle plugin that can be used to repeat QFace -> AIDL conversion on the service end
- Ships small java library that is required to obtain service binder object (at the moment it is not possible to obtain binder using unmodified NDK) and is used by the generated code.
Currently only a subset of QFace IDL is supported. Support for the following major elements of QFace grammar are not implemented yet:
- Nested Types
- Enum/Flag
extends
keyword
Regarding the conversion rules between QFace IDL and AIDL the QtAidl module takes the following convention:
- Module becomes Package
- Struct becomes Parcelable and is resulting in creation of
<StructName>Struct.aidl
file allowing the exchange more complex data - Interface results in creating two AIDL Interfaces and creates the following files:
I<InterfaceName>Service.aidl
I<InterfaceName>Callback.aidl
I<InterfaceName>Service
represents the business logic part that QtAidl backend expects to be implemented by the actual android service:- it has two extra methods (that are called when backend tries to bind/unbind to/from the service) that are taking the instance of
I<InterfaceName>Callback
interface:void registerCallback(IStopwatchControllerCallback cb)
the implementing service shall store the reference tocb
on call to that method for future usevoid unregisterCallback(IStopwatchControllerCallback cb)
the implementing service shall not usecb
any more and shall clear local reference
I<InterfaceName>Callback
provides communication channel from implementing service back to QtAidl backend on application side. It could be treated as a counterpart providing the ability to do theemit signal
if compared to the Qt world- QFace Interface properties rules:
- All the QFace Interface properties will result in:
- adding
<PropetyType> <propertyName>()
method toI<InterfaceName>Service
interface - the implementing service shall return an internally stored value on call to that method - adding
void <propertyName>Change(in <PropertyType> <propertyName>)
method toI<InterfaceName>Callback
interface - the implementing service shall invoke that method on storedcb
reference whenever internally stored value is changed
- adding
- If property is neither
readonly
norconst
the following method will be added toI<InterfaceName>Service
interface:- void
set<PopertyName>(in <PropertyType> <propertyName>)
- the implementing service shall invoke
void <propertyName>Change(in <PropertyType> <propertyName>)
method on storedcb
reference once the value passed in setter was accepted
- void
- All the QFace Interface properties will result in:
- QFace Interface operations rules:
- All the QFace Interface opertations will result in:
- adding
<OperationReturnType> <operationName>(in <OperationArgumentType1> <operationArgument1>, ..., in <OperationArgumentTypeN> <operationArgumentN>)
method toI<InterfaceName>Service
interface - the implementing service shall implement those methods as part of business logic
- adding
- All the QFace Interface opertations will result in:
- QFace Interface signals rules:
- All the QFace Interface signals will result in:
- adding
void <operationName>(in <SignalArgumentType1> <signalArgument1>, ..., in <SignalArgumentTypeN> <signalArgumentN)
method toI<InterfaceName>Callback
interface - the implementing service shall invoke that methods on storedcb
reference in case of events coresponding to given signal
- adding
- All the QFace Interface signals will result in:
- it has two extra methods (that are called when backend tries to bind/unbind to/from the service) that are taking the instance of
- QtAidl module backend expects that implementing service will respond to intent formed as
<module.uri>.<LAST_URI_FRAGMENT>
so if your module will be calledcom.mycompany.contacts
the service shall filter oncom.mycompany.contacts.CONTACTS
intent
In case you'll find misbehavior in the above rules, or you'll discover that some of the rules that apply to QFace/Qt Interface Framework are missing please report an issue or submit Pull Request.
The QtAidl Module is built on top of Qt Interface Framework and beacuse of that it reuqires that module to be present in your local Qt Framework installation. If you're holder of valid Qt for Device Creation Enterprise
commercial license you shall be able to install Qt Interface Framework using the Qt Maintance Tool.
Otherwise, if your project meets the expectations of LGPL v3 license you could install this module from the source. To do so please perform the following steps (all steps were verified using Qt 6.4.0 versions):
- Install Qt 6.4.0 version for both Desktop and Android platforms
- Qt Interface Framework requires
Python3
withpip
andvirtualenv
modules available, ensure that your host enviroment have them installed - Get the Qt Interface Framework source code from git repository
git clone --branch 6.4.0 --recurse-submodules http://code.qt.io/qt/qtinterfaceframework.git
- Open the main
CMakeLists.txt
file in Qt Creator and choose your desktop Qt 6.4.0 Kit as the main configuration - Switch to
Projcts
settings page (1), selectinstall
as build target (2) and pressBuild
button (3) - Now add the Android Kits for the ABIs you're interested in and repeat same steps as for point 5 (ignore
No target architecture defined in json file.
error)
- Once Qt Interface Framework was installed open main QtAidl
CMakeLists.txt
file and select Qt for Android Kits for the ABIs you're interested in (1) and pressConfigure project
button (2) - Switch to
Projcts
settings page (1), selectinstall
as build target (2) and pressBuild
button (3) (ignoreNo target architecture defined in json file.
error) - Repeat for all Kits you're interested in
The QtAidl module provides a minimalistic stopwatch example to demonstrate some of the QtAidl features on both QML application and Kotlin Service side.
The StopwatchApp example is a frontend application that demonstrates how to consume Qt Interface Framework features in the QML files. This application can be built for both:
- Desktop - it is then using the Qt Interface Framework
backend_simulator
project template for the backend (the implementation is placed inexamples/stopwatchapp/middleware/backend_simulator
) - Android - it is then using QtAidl
aidl
project template for the backend (the implementation is placed inexamples/stopwatchapp/middleware/backend_aidl
)
Besides using different backends the code for both variants is exactly the same.
In order to build this application for your host platform in Qt Creator open examples/stopwatchapp/CMakeLists.txt
project, configure it using Kit for which you've installed the Qt Interface Framework module and press Run
button
In order to build this application for android target
- In Qt Creator open
examples/stopwatchapp/CMakeLists.txt
project - Select the Kit for which you've previously installed QtAidl module and press
Configure project
button - Switch to
Projcts
settings page (1), in theCMake Initial Configuration
tab setANDROID_NATIVE_API_LEVEL
to30
(2) and add new optionANDROID_PLATFORM
with valueandroid-30
(3)(actually the first aplies to NDK version below 23.x.x and the second to the newer NDK versions - the AIDL NDK backend is fully functional since Android 11) and pressRe-configure with Initial Parameters
button (4) - When the project is configured run it on a selected device/emulator
In order to generate the code that will allow binding with service implementation you shall create your Qt Interface Framework backend in the same way as shown in examples/stopwatchapp/middleware/backend_aidl/CMakeLists.txt
. The following snippet shows the most important points:
find_package(Qt6 COMPONENTS Aidl) #[1]
#[2]
qt_add_plugin(middleware_backend PLUGIN_TYPE interfaceframework)
#[3]
qt_ifcodegen_extend_target(
middleware_backend
IDL_FILES
${QFACE_SRC}
TEMPLATE
aidl #[4]
PREFIX
MIDDLEWARE)
target_link_libraries(middleware_backend PRIVATE middleware Qt6::Aidl Qt6::Core) #[5]
- First of all you need to find
Qt6::Aidl
package which is provided by QtAidl module - Next you need to create new plugin target with
PLUGIN_TYPE
set tointerfaceframework
- Now using Qt Interface Framework
qt_ifcodegen_extend_target
macro you need to generate the backend code - Make sure that the
TEMPLATE
argument is set toaidl
- Finally make sure your plugin target links against
Qt6:Aidl
module
The provided example is integrating the Example Service (examples/stopwatchservice
) which is packed as the Android Library. The service logic could be also part of the application project or as most optimal solution part of different APK (with limitation that Activity-less APKs are reserved only for the system services). There are three ways to integrate the library into project:
- Use the local aar file
- Add the library subproject
- Use the library that is distributed as Maven package
If you're interested in more details on two first approaches please refer to Qt documentation. For the stopwatch we've chosen the third approach:
- It allows for completely separated development
- Has the ability to version components independently
- Allows to minimize dependencies management (e.g. using local AAR file would require us to repeat all service dependencies in UI application gradle project to avoid runtime failure)
In our example the service package was published to maven repository and integrated into application project on the gradle project level (examples/stopwatchapp/app/android/build.gradle
) in the following way:
repositories {
google()
mavenCentral()
mavenLocal() //[1]
maven {
url 'https://repo.repsy.io/mvn/spyrosoft-synergy/aar/'
} //[2]
}
apply plugin: 'com.android.application'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'com.spyro-soft:stopwatchservice:1.0' // [3]
}
- While assembling the APK gradle will look for additional libraries in local Maven repository (which is useful in case of development)
- Or in remote Maven repository
- The service implementation package name is called
com.spyro-soft:stopwatchservice:1.0
in our example
The example service logic is placed in examples/stopwatchservice
folder.
In order to generate required AIDL files this project is using QtAidl gradle plugin, which was integrated in the following way:
The Maven lookup repositories for the plugin are configured in the examples/stopwatchservice/settings.gradle
file:
pluginManagement {
repositories {
google()
mavenCentral()
mavenLocal() // [1]
maven {
url "https://repo.repsy.io/mvn/spyrosoft-synergy/gradle/" // [2]
}
}
}
- Local repository (which is useful in case of development as plugin has to be rebuilt each time QtAidl jinja2 templates are changed)
- Remote Maven repository
The actual service project file is placed under the following location examples/stopwatchservice/service/build.gradle
plugins {
id 'com.android.library' // [1]
id 'com.spyro_soft.qtaidl' version '0.2.9' // [2]
}
...
android {
...
sourceSets {
main.qface.srcDirs += [projectDir.path + '/../../common'] // [3]
}
...
}
project.afterEvaluate {
publishing {
publications {
maven(MavenPublication) {
groupId 'com.spyro-soft' // [4]
artifactId 'stopwatchservice' // [5]
version = android.defaultConfig.versionName // [6]
The main points to note and repeat are as follows:
- Configure project as the android library (QtAidl plugin can be used also for the projects with type
com.android.application
) - Apply
com.spyro_soft.qtaidl
gradle plugin - Point to the directory which contains QFace file(s)
- In order make the package available as
com.spyro-soft:stopwatchservice:1.0
configuregroupId
[4],artifactId
[5] andversion
[6]
The service logic code is located in the examples/stopwatchservice/service/src/main/java/com/spyro_soft/stopwatchservice/StopwatchService.kt
file:
private val callbacks = RemoteCallbackList<IStopwatchControllerCallback>() // [1]
private val binder = object : IStopwatchControllerService.Stub() { // [2]
override fun registerCallback(cb: IStopwatchControllerCallback?) { // [3]
...
}
override fun unregisterCallback(cb: IStopwatchControllerCallback?) { // [4]
...
}
override fun setIsRunning(isRunning: Boolean) { // [5]
...
}
override fun reset() { // [6]
...
}
override fun time(): TimeStruct { // [7]
...
}
override fun isRunning(): Boolean { // [8]
...
}
}
override fun onBind(intent: Intent): IBinder { // [9]
...
}
rivate fun postIsRunning(isRunning: Boolean) { // [10]
...
}
private fun postTime(time: TimeStruct) { // [11]
...
}
}
The most important parts common to every service implementation are as follows:
- Have a container for registered callbacks (instances of
IStopwatchControllerCallback
) - Create an implementation for
IStopwatchControllerService
stub (which was generated out of AIDL files generated by QtAidl gradle plugin) - Add new callback instance to container
- Remove callback instance from container
- Property setter
- Operation implementation
- Getter for the property of
TimeStruct
type (parcerable) - Getter for another property
- Binder for this service
- Invoke property change signal on the instance of
IStopwatchControllerCallback
- Invoke another property change signal on the instance of
IStopwatchControllerCallback
Rest of the code is just the implementation detail.
In order to register the intent on which service can be bound it shall be specified in the AndroidMainifest file (as the example take a look on examples/stopwatchservice/service/src/main/AndroidManifest.xml
file):
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.spyro_soft.stopwatchservice">
<application>
<service
android:process=":StopwatchService"
android:name=".StopwatchService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.spyro_soft.stopwatchservice.STOPWATCHSERVICE" /> <!-- [1] -->
</intent-filter>
</service>
</application>
</manifest>
- Register the intent filter for the service.
In order to debug the service code:
- Start the StopwatchApp on the android device using Qt Creator
- Open the main service project (
examples/stopwatchservice
) in the Android Studio, pressAttach Debugger to Android Process
button [1] and selectcom.spyro_soft.stopwatchapp:StopwatchService
service on the processes list [2]
If there is a need to test local changes made for either gradle-plugin or stopwatch service, one shall invoke publishToMavenLocal
gradle task e.g. from command line from within particular project root folder(src/tools/gradle-plugin
or examples/stopwatchservice
):
./gradlew publishToMavenLocal
The hidden heroes
Although most of their work remains hidden behind internal SpyroSoft repositories this project wouldn't be created without the active participation from:
- Mateusz Wierzchowski - author of the first POC implementation
- Łukasz Ornatek - author of the QFace -> AIDL conversion
- Marcin Wojtynka - author of
aidl
backend core implementation - Irina Rychkova - UI/UX designer of the StopWatch Application
- Szymon Wojtach - author of the QML implementation
- Filip Zymek - author of the Kotlin service (and Flutter based UI frontend which shows that using AIDL you can not only switch backends but also frontends easily)
- Marek Mazur - author of gradle plugin
QtAidl is available under the MIT license
QtAidl module potentially contains or uses third party modules under the following licenses:
Library | License |
---|---|
Qt Interface Framework | Qt Commercial License or GNU Lesser General Public License, version 3 |
QFace | MIT license |
Miniconda | 3-clause BSD license |