Published:

Jarle Aase

Targeting Android with the Nextapp QT/QML application

bookmark 7 min read

I use cmake for my C++ projects. That's also what Qt Creator use these days. However, QT adds lots of macros and functions when you add the qt_standard_project_setup() statement. Some of those macros and function may not work the way you expect or may contain surprising bugs.

Making a simple QT app work with Android is trivial. Making a real app work is not so trivial. This is the first article in this series to address Android specifically. There will probably be more ;)

Red flags

One thing that became clear to me pretty soon when I adapted NextApp to Android is that Android is not a high priority for the Qt Project. It is not well documented how to get an application up and running smoothly with Android. And even if Qt Creator has excellent support for using the Android emulator, QML application running in the emulator simply does not work. Some pretty basic QT controls does not render their data.

Fortunately QT creator also have excellent support for installing and debugging apps on real Android devices, so I can still test my applications effectively.

I am using the new gGPC feature (still in technical preview). That was totally broken for Android in 6.8 beta 1 - 3. (It works on all desktop versions.) This indicate that they don't have any automates tests on Android for new features.

Another surprise for me was that drag & drop is broken on Android. (I have only tested this on 6.7, since I need gRPC in my app. It may be fixed in 6.8). What happens is that when I grab something, like a list item, to drag it, the visual image of the dragged item appears on a random location on the screen, and not where my finger is. Then the drag operation gets stuck, and I have to touch the item on it's random location to finish the drag event and have it rendered in it's original position.

QML Window size and position

QML components expect variables for size and screen position. The QML language does not use the C++ preprocessor to allow different platforms to use different values. For example, a QML app on desktop must set the hight and with to reasonable values. But on an Android device, you typically want it to use the entire screen. So, for a main window, I ended up with this code just to position then window correctly:

1Window  {
2    id: root
3    width: Math.min(600, NaCore.width, Screen.width)
4    height: Math.min(800, NaCore.height, Screen.height)
5...
6}

As you see, I have added screen width and hight to my own component (actually a C++ component) to deal with this.

I have not stumbled over examples that have given a simpler way to deal with it. Personally, I would have preferred something like

 1Window  {
 2    id: root
 3#if defined(ANDROID) || defined(IOS)
 4    width: Screen.width
 5    height: Screen.height
 6#else
 7    width: 600
 8    height: 800
 9#endif
10...
11}

Using defines would also be useful in some cases where some of my QML components use parent layouts on desktop, but not under Android - so that I could use:

 1Window  {
 2    id: root
 3#if defined(ANDROID) || defined(IOS)
 4    anchors.fill: parent
 5#else
 6    Layout.fillWidth: true
 7    Layout.fillHeight: true
 8#endif
 9...
10}

Manifest

Android apps require a AndroidManifest.xml file. How and where to add this file is not well documented. I asked ChatGPT for information about this and wasted an hour on it's delusions. Eventually I was able to hack something together based on information from QT documentation, Stack Overflow and trying and failing.

I ended up adding an android folder under the projects root directory with a AndroidManifest.xml and a res sub-folder. Android Studio was able to generate all the various sizes of the applications icon that Android require. (I don't get it. Why don't Android just use vector graphics for icons?)

The current file looks like this:

 1<?xml version="1.0"?>
 2<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="eu.lastviking.appNextAppUi" android:versionCode="-- %%INSERT_VERSION_CODE%% --" android:versionName="">
 3
 4    <!-- Permissions -->
 5    <uses-permission android:name="android.permission.INTERNET"/>
 6    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
 7    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 8    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 9    <uses-permission android:name="android.permission.CAMERA"/>
10    <uses-permission android:name="android.permission.READ_CONTACTS"/>
11    <uses-permission android:name="android.permission.SEND_SMS"/>
12
13    <!-- %%INSERT_PERMISSIONS -->
14
15    <!-- %%INSERT_FEATURES -->
16
17    <!-- Required Qt metadata -->
18    <application android:name="org.qtproject.qt.android.bindings.QtApplication" android:hardwareAccelerated="true" android:label="appNextAppUi" android:requestLegacyExternalStorage="true" android:allowBackup="true" android:fullBackupOnly="false" android:icon="@drawable/icon">
19            <activity android:name="org.qtproject.qt.android.bindings.QtActivity" android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" android:launchMode="singleTop" android:screenOrientation="unspecified" android:exported="true" android:label="">
20            <intent-filter>
21                    <action android:name="android.intent.action.MAIN"/>
22                    <category android:name="android.intent.category.LAUNCHER"/>
23                </intent-filter>
24            <meta-data android:name="android.app.lib_name" android:value="appNextAppUi"/>
25            <meta-data android:name="android.app.arguments" android:value=""/>
26        </activity>
27
28            <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.qtprovider" android:exported="false" android:grantUriPermissions="true">
29                <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/qtprovider_paths"/>
30            </provider>
31    </application>
32</manifest>
33

QT will expand some macros, but it does so unintelligently. For example, %%INSERT_PERMISSIONS will insert certain permissions, even if they are already present - causing an error.

Qt Creator has a GUI for the manifest, but it's not complete, and it may add items to the file, like android:versionCode="" that breaks the build.

How to test the app

As mention above, the Android emulator did not work well for my QML app.

Out of the box my phone was not found by neither Android Studio or QT Creator. None of them gave any hints about any problems.

Examining the Linux system log, I saw this:

19.06.24 г. 14:25 ч.	plasmashell	2024-06-19 14:25:25,053 [ 953410]   WARN - #com.android.ddmlib - 06-19 14:25:23.919 19131 19131 I adb     : transport.cpp:1150 H0T9FI1LV0171300139: connection terminated: failed to open device: Access denied (insufficient permissions)
...

19.06.24 г. 14:25 ч.	plasmashell	'/opt/Android/sdk/platform-tools/adb start-server' failed -- run manually if necessary

So I tried to get adb to list the phone connected via USB.

$ /opt/Android/sdk/platform-tools/adb devices
daemon not running; starting now at tcp:5037
ADB server didn't ACK
Full server startup log: /tmp/adb.1000.log
Server had pid: 13864
--- adb starting (pid 13864) ---
06-19 14:14:14.945 13864 13864 I adb : main.cpp:63 Android Debug Bridge version 1.0.41
06-19 14:14:14.945 13864 13864 I adb : main.cpp:63 Version 35.0.1-11580240
06-19 14:14:14.945 13864 13864 I adb : main.cpp:63 Installed as /opt/Android/sdk/platform-tools/adb
06-19 14:14:14.945 13864 13864 I adb : main.cpp:63 Running on Linux 6.5.0-41-generic (x86_64)
06-19 14:14:14.945 13864 13864 I adb : main.cpp:63
06-19 14:14:14.948 13864 13864 I adb : auth.cpp:416 adb_auth_init...
06-19 14:14:14.948 13864 13864 I adb : auth.cpp:152 loaded new key from '/home/jgaa/.android/adbkey' with fingerprint 5CBA9E751EA48B69B67A9FC6B5FC43DB4735A4CA31C989BBA4362E35C72DDA67
06-19 14:14:14.948 13864 13864 I adb : auth.cpp:391 adb_auth_inotify_init...
06-19 14:14:15.949 13864 13864 E adb : usb_libusb.cpp:571 failed to open device: Access denied (insufficient permissions)
06-19 14:14:15.949 13864 13864 I adb : transport.cpp:1150 H0T9FI1LV0171300139: connection terminated: failed to open device: Access denied (insufficient permissions)

failed to start daemon
adb: failed to check server version: cannot connect to daemon

I consulted ChatGPT which suggested to create this file: /etc/udev/rules.d/51-android.rules and add the following line:

SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"

Note that the vendor-id is significant. You have to sudo lsusb to see what vendor your phone reports.

For my phone, I got this line:

Bus 009 Device 011: ID 18d1:4ee7 Google Inc. Nexus/Pixel Device (charging + debug)

And as you can see, the vendor ID is the first hex number in the ID.

Now I had to reload the device rules:

1sudo udevadm control --reload-rules
2sudo service udev restart

After that, the phone appeared as a device in Qt Creator, and I could deploy to it.

Running the "Android" version on the desktop

Deploying an Android version in the emulator or on a real Android device takes a lot more time than just compiling and running the app on my workstation. So one of the first things I did when I started to work on the Android version was to add a compile time flag to pretend I was compiling for Android.

For example. The desktop version use a main.qml file that expects a big screen with room for a vertical navigation bar on the left, a tree view for the lists, the a list of actions, and rightmost the appointments and time allocations for a whole day. In other words, at least 1200 pixels width on a normal monitor. A phone, in comparison, has about 350 normal points. It probably has more physical pixels, but they are small. So what's practical to display on the phone is similar to the information we can show in about 350 pixels on a monitor. So the Android version of Next-App use another main.qml that is designed for a phone. Similarly, while the desktop version use dialogs with labels on the left and input fields on the right, the dialogs with Android show the labels above the input fields.

Some views are used exclusively with desktop or Android, others adopt. I have added a const attribute in a static C++ model for QML that has a boolean value to tell the QML components if they are using a mobile device. This allow for code like this to make a dialog adaptable.

 1Dialog {
 2    id: root
 3    property int controlsPreferredWidth: (width - 40 - leftMarginForControls) / (NaCore.isMobile ? 1 : 4)
 4    property int labelWidth: 80
 5    property int leftMarginForControls: NaCore.isMobile ? 20 : 0
 6
 7    x: Math.min(Math.max(0, (parent.width - width) / 3), parent.width - width)
 8    y: Math.min(Math.max(0, (parent.height - height) / 3), parent.height - height)
 9    width: Math.min(600, NaCore.width, Screen.width)
10    height: Math.min(700, NaCore.height, Screen.height)
11
12    ...
13            GridLayout {
14                Layout.alignment: Qt.AlignLeft
15                Layout.fillHeight: true
16                Layout.fillWidth: true
17                rowSpacing: 4
18                columns: NaCore.isMobile ? 1 : 2
19
20                Label {
21                    text: qsTr("Created")
22                }
23
24                Text {
25                    Layout.leftMargin: root.leftMarginForControls
26                    id: createdDateCtl
27                }
28
29                Label {
30                    text: qsTr("Completed")
31                }
32
33                Text {
34                    Layout.leftMargin: root.leftMarginForControls
35                    id: completedTimeCtl
36                }
37
38    }
39}

A compile-time option tells the C++ code to pretend we are using a mobile device, and it starts the app using Android's main.qml and turns on the isMobile flag for all adaptable QML components.

This was a time saver! I still need to test with a real Android device to see that components are working as expected. For example, while dragging is a natural part of a desktop application, I had to turn it off under Android, since QML cannot distinguish between a drag operation and a user just wanting to scroll a view.

So even I the first iteration of my QML code was pretty general, I had to adapt and tweak almost all the files to work well with Android.