Android Java Bindings in Qt 6.7

The Qt for Android plugin, introduced more than a decade ago, has been a game-changing change that opened a multitude of possibilities for developers looking to harness the power and flexibility of Qt for Android application development. Since then, many Android, Qt, and plugin changes have been made to support new features. However, neither the overall architecture nor the public Java bindings have changed much. These bindings contain wrappers for the Android Activity. It is about time we did that! 

Over time, the Android plugin has become difficult to maintain and debug, making it hard to introduce new features or changes without introducing regression bugs. Additionally, the previous Qt for Android Java bindings had a bit of a convoluted hierarchy and relations that could even impact performance. Under Qt 6.7, efforts have been made to address these issues and achieve several desired outcomes. These include simplifying the architecture and making the plugin's Java code more modular, which would help us fix critical bugs and enhance future support for newer Android versions. 

The Public Java Bindings

The Java bindings are Qt's wrapper classes around the native ActivityServiceApplication classes, and others responsible for loading the Qt shared libraries when starting the Android app. The bindings are referenced by the build tools and are built under every Qt for Android app. One of the critical issues of the previous implementation is different classes not having clear responsibilities. 

Loading the Native Libraries

Let us first look at loading the Qt libraries logic, which was scattered over multiple classes with logic duplication between them. For example, when loading an Android shared library with  System.load(), the native C++ JNI_OnLoad() is called. However, that usually requires some initializations and meta-data to be parsed beforehand. The QtLoader binding class takes care of parsing the app's meta-data. Then, somewhere in QtActivityDelegate, we invoke the library loading logic that lives under QtNative, and then we go back to the QtLoader, which invokes the native app startup. That is quite a convoluted process. To improve that, we have shuffled a few things around. First, the loader classes are of no use to the users to be public, so we have moved them to be an internal implementation under the QtAndroid Jar. Then, we made QtLoader responsible for the following now: 

  • Initializing the class loader that QJniObject uses. 
  • Parsing the app's meta-data, for example, the list of Qt libraries used by the app or the app's library name. 
  • Setting various environment variables and themes and assembling the app's arguments list. 
  • Handling initialization specific to the Activity or Service from QtActivityLoader or QtServiceLoader, respectively. 
  • Loading the Qt libraries and then the main library. 

The Activity and Service Wrappers

With the loaders being handled now, we are left with QtActivity, QtService, and QtApplication, which extend Android's Activity, Service, and Application, respectively. The immediate thing you notice about them is they are quite big, especially QtActivity, and they contain quite a lot of duplicated code and Java reflection. The idea behind that was the Qt implementation details of the Activity's various functionalities are kept under a private QtActivityDelegate under the Qt for Android package, and then upon app startup, references to those methods are cached. Let's take an example of the Qt Activity's onActivityResult() implementation. Initially, the method is retrieved and cached under QtActivityDelegate:


m_super_onActivityResult = activityClass.getMethod("super_onActivityResult", Integer.TYPE, Integer.TYPE, Intent.class);

Then, under the same class, we have an implementation similar to this:


public void onActivityResult(int requestCode, int resultCode, Intent data)
{
    try {
        m_super_onActivityResult.invoke(m_activity, requestCode, resultCode, data);
    } catch (Exception e) {
        e.printStackTrace();
    }

    QtNative.onActivityResult(requestCode, resultCode, data);
}

The QtActivity class, under the user's project package, can then use reflection to call that method using a helper method under QtApplication:


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{

    if (QtApplication.m_delegateObject != null && QtApplication.onActivityResult != null) {
        QtApplication.invokeDelegateMethod(QtApplication.onActivityResult, requestCode, resultCode, data);
        return;
    }
    super.onActivityResult(requestCode, resultCode, data);
}
public void super_onActivityResult(int requestCode, int resultCode, Intent data)
{
    super.onActivityResult(requestCode, resultCode, data);
}

Notice all the boilerplate code that needs to be done for each method override for both the Activity and Service.

Now, after changes made in Qt 6.7, we will deal with that in the following steps. Introduce a QtActivityBase wrapper under the Qt Android package that implements Qt's custom functionalities on top of the default Activity:


public class QtActivityBase extends Activity
{
    ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        super.onActivityResult(requestCode, resultCode, data);
        QtNative.onActivityResult(requestCode, resultCode, data);
    }
    
    ...
}

And then QtActivity could simply extend that:


public class QtActivity extends QtActivityBase
{
    ...
}

This saves us from making expensive reflection calls that used to take more than a thousand lines of code. It avoids the huge code duplication altogether and keeps the public QtActivity very lightweight. This refactoring avoids reflection usage and tremendously simplifies the path of execution for Qt for Android apps.

Division of Responsibilities

Following the footsteps described in the previous sections, all corners of the Android plugin's Java code have seen changes that take various classes from complex do-it-all classes to ones with clear modularized responsibilities that deal with display, clipboard, keyboard management, etc. This, of course, is not a one-time effort but rather the start of more improvements that will continue and that will allow us to easily keep the Qt Android port up to date with features from Android. 

An example of such features is adding proper support for child windows for Android, which is a prerequisite for enabling the embedding of QML apps in native non-Qt Android apps. There will be a separate article that talks about this in more detail coming up soon.

Documentation

To conclude the work, we took care of the documentation as well, where we did a complete revision of the Qt for Android documentation pages, keeping it up to date, omitting misleading or inaccurate information, and making it easier to navigate straight to the point with less duplication. Also, we documented the Android port architecture and how it works behind the scenes under the How Qt for Android Works page. 

You can also check out Qt for Android changes and new features at What's New in Qt 6.7.


Blog Topics:

Comments