środa, 18 listopada 2015

Story of porting complex Qt application to Android - part II

First part of the story is here

7. Scrolling

Unfortunately, with "Fusion" Qt style all scroll bars look the same like on desktop, but with "android" style they are big enough to be touched and it is looking... ugly (It is only my personal opinion)
...and Have you seen such bars in Android applications?
My solution is to get rid of them at all. Another idea is to stylizing them to be just thin - similar to native Android bars.
Anyway, both solutions take away the possibility to scroll - when bars are invisible or impossible to touch.

To have nice, single finger scrolling - as befits Android app - we need QScrollArea wrapped by QScroller -
for any window, tab, stacked widget page or so, whitch is going to be bigger than available screen space.
Also for QListWidget or QListView or QTextEdit or any widget inherits from QAbstactScrollArea we may use QScroller
QScroller::grabGesture(any_Scrolable_Widget->viewport(), QScroller::LeftMouseButtonGesture);
Yes, We do not need any fancy touch gesture, QScroller::LeftMouseButtonGesture is far enough to work with mouse button and finger touch.

So, what about not scrolable widgets. In Nootka there are a lot of config and creators widgets. All of them are deriving from simple class made of QScrollArea

Then we use TtouchArea as an ordinary widget, adding it to any widget container classes.


8. Quicker deploying

It could be very absorbing to watch all communicates when Qt Creator is building an application and deploying it into Android device. But only a few times at the beginning. Later on, when one realize how much time it takes, it is simply annoying.
So what can we do to short this process.

- Using adb to push a part of apk package

All hints below require rooted device, but every emulated ones are rooted by default.
In Android world our Qt application is just a library, so instead of creating apk package and deploying it once more time, we may copy it directly to location where it has been installed already:
$adb push build/path/libOurApplication.so /data/data/app.our.of.domain/lib/
Just after that we may launch the fresh built of the app directly in emulator or from Qt Creator.

- Dividing the app library

Sending megabytes of data through adb takes time, so above solution may be still not enough due to big size of our app library in debug mode.
It can be one more reason why we should divide our code on libraries.
Then, after small change in the code, which affects only one of the libraries we are coping only that one.
Also rich resources of our application (icons, audio files and so) included to the lib by qrc system increases its size much.
All of that can be packed into separate library.
Another way is to use assets instead of qrc. In Nootka there are a few ogg files with samples for different kinds of guitars and only one of them is used (depends on user settings) and it is loaded once, during launching. Using assets is much suitable for this purpose. The same is used for translations (qm files)

9. Debugging trick

It is very usable to run an application in terminal and to see some debug messages. We may run our app in Qt Creator and watching messages but we also may se what our app says by grabbing messages into log file. To achieve that a function i.e. myMessageOutput has to be created before main() in main.cpp


Then it has to be installed by qInstallMessageHandler (usually in main)

This way, in device memory a file myApp-log.txt is created for every run with all messages of our app.

10. File dialog

On desktop one uses to call
QFileDialog::getOpenFileName()
to get native file dialog and file name then. But there is no Android native file dialog, so ordinary Qt file dialog will appear.
Under mobile system such a dialog is completely not suitable for it, even worst - it is dangerous, because if one want to scroll listed context of some directory with one finger, some directory under the finger may be dragged into another one.
The simplest way I found to get a file name is to invoke external Android application related to file manager through JNI.
Here is a post about it:
http://stackoverflow.com/questions/15079406/qt-necessitas-reasonable-qfiledialog-replacement-skin

But it is really not difficult task to implement own file dialog. Here is what Nootka has:

Only thing to get it is to have some dialog with QListView and set its model to QFileSystemModel
listView = new QListView(this);
fileModel = new QFileSystemModel(this);
listView->setModel(fileModel);
Rest of the dialog depends on our needs.
Here is Nootka's implementation of file dialog:
https://github.com/SeeLook/nootka/blob/master/src/libs/core/widgets/tfiledialog.h
https://github.com/SeeLook/nootka/blob/master/src/libs/core/widgets/tfiledialog.cpp


11. Color dialog

A dialog window available by calling
QColorDialog::getColor()
is not suitable for touch screens at all. QML has better optimized color dialog but how to get this under QtWidgets?
There is a project with alternative implementation of color dialogues for Qt:
https://github.com/mbasaglia/Qt-Color-Widgets

With a few changes I get such a result on Android:


Here is a code:
https://github.com/SeeLook/nootka/blob/master/src/plugins/settings/tcolordialog.h
https://github.com/SeeLook/nootka/blob/master/src/plugins/settings/tcolordialog.cpp

It uses color wheel of Qt-Color-Widgets:
https://github.com/SeeLook/nootka/blob/master/src/plugins/settings/color_wheel.hpp
https://github.com/SeeLook/nootka/blob/master/src/plugins/settings/color_wheel.cpp




niedziela, 15 listopada 2015

Story of porting complex Qt application to Android - part I

1. Abstract

In this post (or posts) I'm going to describe process of porting Nootka to Android.
In a few chapters I will try to summarize steps of that process, write about some issues I had to faced with and possible solutions.
I hope it would help someone to avoid inventing a wheel once more time and to realize some questions that will appear during porting any Qt application to Android, also for myself, to better understand all of that.
Let me know if You think there is something wrong or You know some better way.


What Nootka is - read here
but from programming point of view there are some details about desktop versions:
  • C++ and QtWidgets are used but not QtQuick
  • Currently Qt 5.5 is on the board
  • For coding Kdevelop is used (under Arch Linux)
  • Code is quite big (more than 400 files, 60k lines)
  • Project is divided on three libraries and a few plugins
  • External libraries are used (fftw, soundtouch, ogg and RtAudio)
  • There is no any UI files, all layout is created in c++ code

2. Preparations

Before we start, it is recommended to read KDAB blogs about Qt under Android:
https://www.kdab.com/qt-on-android-episode-1/

3. The foundations - build environment

Qt Creator

In spite Qt Creator and entire Qt is easy available under any Linux distro, for Android purposes I'm using official Qt on-line installer. This way I have ready build environments for Android platforms (armv7 -real devices and x86 - emulator).

I'm still coding with Kdevelop but to easy deploying to Android device and debugging, I'm doing this from Qt Creator.
Also Qt Creator makes all Android related things automatically (generates manifest file and building final apk)

qmake

Desktop version is built with cmake but for Android I migrated to qmake.
Actually it is much easier to use cmake for Android projects than one or two years before (with Extra Cmake Modules) so in some future I will back to it.
Anyway, cmake requires more knowledge about how Android apk-s are built when qmake does it almost transparently.

Genymotion

To test the application I'm using Genymotion - Android emulator based on VirtualBox.
Unfortunately, my CPU is not so young (3 cores AMD Phenom 1) and to get Qt working on emulated Android x86 I had to recompile x86 Qt kit (due to lack of ssse3 extensions in that CPU).
To achieve that I cloned qt as is described here:
https://wiki.qt.io/Qt5ForAndroidBuilding
then configured the build with:
configure -xplatform android-g++ -no-sse3 -no-ssse3 -no-sse4.1 -no-sse4.2
 -android-arch x86 -nomake tests -nomake examples -android-ndk path_to_ndk
 -android-ndk-host linux-x86_64 -android-toolchain-version 4.8 -skip qttranslations
 -skip qtwebkit -skip qtserialport -skip qtwebkit-examples -no-warnings-are-errors
This hack is required to get QtMultimedia working in Qt 5.5 and above. QtWidgets are free from ssse3 dependencies.

4. Screen orientation, scaling and other issues

Orientation

As long as Nootka window size is well scalable on desktops - I decided to force landscape orientation on possible small phone screens. Guitar fingerboard requires it.
It is done by squeezing an entry
android:screenOrientation="landscape"
into Activity node in AndroidManifest.xml file.
This way I got rid of scaling issues occurring when phone or tablet changes its orientation.

Full screen and screen lock

To have whole screen for the app and stop screen going to sleep, there is a function using QAndroidExtras module that is invoked when the app is launching:


Also additional permission:

Has to be set to allow keeping screen on. Into AndroidManifest.xml file as well.

Of course, main window has to be set to full screen:


Dialog windows

Also any dialog window will be rather maximized:


5. Main window and layout issues

Probably, if I would be about to start again from scratch I might use QtQuick for GUI layout but porting to it - no, no it was to expensive.
Fortunately, some decisions about layout I was made long before any Android related thought come to my mind - saved many of efforts.
What was that?

To have a possibility of painting over main window, to display text boxes and to perform some fancy animations a central widget of QMainWindow is type of QGraphicsView.
auto innerWidget = new QGraphicsView(this);
innerWidget->setScene(new QGraphicsScene(innerWidget));
setCentralWidget(innerWidget);
Then to this innerWidget QGraphicsProxyWidget is added that wraps QWidget container with all staff.
auto container = new QWidget;
// add some widgets into it
QGraphicsProxyWidget *proxy = scene()->addWidget(container);
To keep those in proper size, resizeEvent() is re-implemented:
void MainWindow::resizeEvent(QResizeEvent*) {
  proxy->setGeometry(0, 0, width(), height());
  scene()->setSceneRect(0, 0, width(), height());
  container->resize(size());
}
Note: to have those sizes valid all layouts must to have content margins set to 0.
someLayout->setContentsMargins(0, 0, 0, 0);
But under small Android screens saving space is rather a good habit.

6. In good style

Since Qt 5.4 version Qt offers "android" style but after many attempts to adjust it for Nootka purposes I get rid, and re-implemented Fusion style.
I will keep eye, maybe in further Qt releases it will be more matured.
However, the following hacks may be usable also for applications where android style is suitable enough.



To force minimal height of any GUI element the following trick is used:
qApp->setGlobalStrut(QSize(qApp->globalStrut().width(), qMax(qApp->globalStrut().height(), fingerPixels())));
It improves touch interaction and layout look on smaller screens.

Example implementation of QProxyStyle subclass may look like:


This is still not all and a bit clumsy but with this directions it will become more pretty.

Part II is here:
http://nootka-app.blogspot.com/2015/11/story-of-porting-complex-qt-application_18.html

czwartek, 13 sierpnia 2015

Android, QAudioOutput and callback in separate thread.


QAudio (both, either output and input) was something I've been fed up very quickly on desktop. I prefer RtAudio and Nootka works with it pretty good.
But I gave a once more try of it under Android, to keep hands away from Jni.
The start was painful.
QIODevice *outDev = audioOutput-<start();

Returned QIODvice outDev was very "unresponsive". audioOutput->bytesFree() returned only declared buffer size but not real amount of free data. outDev device didn't emit any signals to inform about need of a new audio data,
even audioOutput device didn't emit notify() signal.
The only way to feed device with data was a QTimer event loop and invoking outDev->write().

Another way is tricky.
Instead of starting output with QIODevice *outDev = audioOutput->start ();
we may start it like this:
audioOutput->start(ourDevice);
where ourDevice is an instance of tricky QIODevice subclass.
When "real device" will want a data, it will call
ourDevice->read(char* data, qint64 maxLen);
As Qt doc says: http://doc.qt.io/qt-5/qiodevice.html
by re-implementation of readData(char* data, qint64 maxLen) we get access to it.
Moreover, this call is performed in different thread - "the real device" one.
We may feed char* data just in readData() method, but I prefer to emit signal
and write data in more suitable place.

Important thing! Such signal has to be connected with any slot with
Qt::DirectConnection flag:
connect(ourDevice, &ProxyDevice::feedAudio, someObject, &SomeObject::feedAudioSlot, Qt::DirectConnection);


class ProxyDevice : public QIODevice
{

Q_OBJECT

public:
/** Constructor does almost nothing, just initializes size of fake buffer. */
ProxyDevice(QObject* parent = 0) : QIODevice(parent), m_bufferSize(2048) {}

/** In fact, there is no any buffer!
* That value controls size of data to get by feedAudio()
* instead of maxlen value sending in readData(data, maxlen) */
void setBufferSize(qint64 s) { m_bufferSize = s; }
qint64 bufferSize() const { return m_bufferSize; }

signals:
/** char* is pointer to data, qint64 is its size 
* and qint64& is reference where connected slot will return size of data has been written.  */
void feedAudio(char*, qint64, qint64&);

protected:

/** This is heart of the class:
* feedAudio(data, m_bufferSize, wasRead) signal is emitted
* to get m_bufferSize bytes into data
* and into reference of wasRead connected slot
* returns amout of bytes were put. */
virtual qint64 readData(char *data, qint64 maxlen) {
Q_UNUSED(maxlen)
qint64 wasRead = 0;
emit feedAudio(data, m_bufferSize, wasRead);
return wasRead;
}

/** Dummy - does nothing */
virtual qint64 writeData(const char *data, qint64 len) { return 0; }

private:
qint64          m_bufferSize;

};

When connected slot will not write any data and returns 0 in reference - device will go into Idle state.


sobota, 24 listopada 2012