Доброго времени суток всем посетителям и пользователям Хабра!

Недавно я на эмоциях опубликовал статью, где я рассказывал о всех своих злоключениях при попытки создать qt приложение ( а именно вызвать и использовать
QApplication a(argc, argv);
и использовать его при помощи андроид студии. Было найдено «решение», которое было чрезвычайно костыльным. Теперь у меня были выходные, чтобы разобраться как надо работать с qt без таких костылей из андроид студии. Всем кому интересно — добро пожаловать под кат!

Постановка задачи


Итак, прежде чем двигаться дальше и для всех, кто не читал мою первую статью или забыл, напомню что мне надо сделать. У меня есть ios приложение на objective c, которое для расчёта и прорисовки некоторых вещей использует qt библиотеку. Подчёркиваю. Именно ios приложение и небольшая библиотека на qt (в первой статье я криво выразился и меня поняли, что у меня qt приложение было под ios. Нет. На qt только небольшая библиотека). Мне надо портировать его на андроид. Я благополучно реализовал графический интерфейс, логику, подключил qt библиотеку. В чём проблема? Проблема в том, что когда пытаюсь вызвать JNI функцию из Java части, а эта функция выполняет следующий код
QApplication a(argc, argv);
то я получаю исключение:
This application failed to start because it could not find or load the Qt platform plugin «android».

Мне необходим вызов конструктора qt приложения для того, чтобы я мог рисовать текст в qt. Поэтому мне необходимо разбираться с этой проблемой. Вариант: «Всё переписать на qt» меня не устраивал (ведь приложение уже было готово на Java в андроид студии), поэтому, после много-много часов мною был предложен варинт: создать в QtCreator проект под андроид. Собрать из него aar и импортировать его в андроид студию и пользоваться им (подробнобности всей этой кухни в предыдущей статье). Очевидно, что это решние… как бы помягче выразиться… велосипедно-костыльное. И более того, как оказалось содержащее изъян ( о нём в ps. первой статьи). Поэтому, здесь разберём как добиться нужного эффекта более правильным способом.

Решение


Вся кухня с qt для андроида работает в предположении, что у нас есть 2 класса:
  • QtApplication
  • QtActivity

Поэтому, нам необходимо их добавить к себе в проект. Эти два класса можно найти в папке, где установлено qt для андроида. У меня это C:\Qt\QT.Android\5.5\android_armv7\src\android\java\src\org\qtproject\qt5\android\bindings.
QtApplication можно добавить себе в проект, только заменить там следующие участки кода:

 ИМЯ_КЛАССА_НАШЕЙ_QT_АКТИВИТИ.class.getDeclaredMethod(delegateMethod.getName(), delegateMethod.getParameterTypes());
и
if (-1 == stackDeep) {
            String activityClassName = ИМЯ_КЛАССА_НАШЕЙ_QT_АКТИВИТИ.class.getCanonicalName();
            for (int it=0;it<elements.length;it++)
                if (elements[it].getClassName().equals(activityClassName)) {
                    stackDeep = it;
                    break;
                }
        }

Отлично. Перейдём теперь к классу QtActivity. Всё, что там есть, должно быть и у нашей QtActivity. В моём случае это был MainViewController, в который я скопипастил весь код из QtActivity. Дальше, обратим внимание на метод OnCreate…

super.onCreate(savedInstanceState);

        try {
            m_activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
            for (Field f : Class.forName("android.R$style").getDeclaredFields()) {
                if (f.getInt(null) == m_activityInfo.getThemeResource()) {
                    QT_ANDROID_THEMES = new String[] {f.getName()};
                    QT_ANDROID_DEFAULT_THEME = f.getName();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            finish();
            return;
        }

        try {
            setTheme(Class.forName("android.R$style").getDeclaredField(QT_ANDROID_DEFAULT_THEME).getInt(null));
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (Build.VERSION.SDK_INT > 10) {
            try {
                requestWindowFeature(Window.class.getField("FEATURE_ACTION_BAR").getInt(null));
            } catch (Exception e) {
                e.printStackTrace();
            }
        } else {
            requestWindowFeature(Window.FEATURE_NO_TITLE);
        }

        if (QtApplication.m_delegateObject != null && QtApplication.onCreate != null) {
            QtApplication.invokeDelegateMethod(QtApplication.onCreate, savedInstanceState);
            return;
        }

        m_displayDensity = getResources().getDisplayMetrics().densityDpi;

        ENVIRONMENT_VARIABLES += "\tQT_ANDROID_THEME=" + QT_ANDROID_DEFAULT_THEME
                              + "/\tQT_ANDROID_THEME_DISPLAY_DPI=" + m_displayDensity + "\t";

        if (null == getLastNonConfigurationInstance()) {
            // if splash screen is defined, then show it
            if (m_activityInfo.metaData.containsKey("android.app.splash_screen_drawable"))
                getWindow().setBackgroundDrawableResource(m_activityInfo.metaData.getInt("android.app.splash_screen_drawable"));
            else
                getWindow().setBackgroundDrawable(new ColorDrawable(0xff000000));

            if (m_activityInfo.metaData.containsKey("android.app.background_running")
                && m_activityInfo.metaData.getBoolean("android.app.background_running")) {
                ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=0\t";
            } else {
                ENVIRONMENT_VARIABLES += "QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED=1\t";
            }
            startApp(true);
        }
    }

Здесь есть тонкость. Где мы будем загружать разметку из layouta? То есть, куда вставить следующий участок кода:
 setContentView(R.layout.form_name);
Если, его вставлять до startApp(true);, то ничего не заработает! Поэтому, я просто посоветую пока здесь без объяснений сделать нечто вроде такого:
            startApp(true);
        }
 new Thread(new Runnable() {
            @Override
            public void run() {
                while (!MyLibSo.isQtReady()) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        initializeGUI();
                    }
                });
            }
        }).start();

Здесь MyLibSo.isQtReady() — моя нативная функция. Её основная задача проверять, было ли уже создано в с-части QApplication. Если создано, то запускаем рисование нашего layouta в initializeGUI(); Там уже и вызываем
setContentView(R.layout.form_name);

Размещение qt либ


Теперь наиболее интересная часть. Главная проблема возникает в том, что не грузятся по-нормальному qt-плагины ( в предыдущей статье об этом расписано). Скопипащенный код из QtActivity и QApplication должен решить все эти проблемы. Но для этого ему нужны либы. Где их взять?
  • Скачать из интернета.
  • Указать Qt, где это всё лежит.
  • Загрузить из apk

Второй вариант удаляем сразу — мало ли, пользователь удалит каталог и всё. Первый вариант чуть более интересный. Он осуществляется при помощи сервиса Ministro. Нас будет интересовать третий вариант. Для начала идём в строковые ресурсы и определяем следующее:

   <string name="ministro_not_found_msg">Can\'t find Ministro service.\nThe application can\'t start.</string>
    <string name="ministro_needed_msg">This application requires Ministro service. Would you like to install it?</string>
    <string name="fatal_error_msg">Your application encountered a fatal error and cannot continue.</string>
    <array name="qt_sources">
        <item>https://download.qt-project.org/ministro/android/qt5/qt-5.4</item>
    </array>


    <array name="bundled_libs">
        <item>MyLibWithQt</item>
    </array>

    <array name="qt_libs">
        <item>gnustl_shared</item>
        <item>Qt5Core</item>
        <item>Qt5Gui</item>
        <item>Qt5Widgets</item>
    </array>

    <array name="bundled_in_lib">
        <item>
            libplugins_platforms_android_libqtforandroid.so:plugins/platforms/android/libqtforandroid.so
        </item>
        <item>libplugins_platforms_libqminimal.so:plugins/platforms/libqminimal.so</item>
        <item>libplugins_platforms_libqoffscreen.so:plugins/platforms/libqoffscreen.so</item>


    </array>


    <array name="bundled_in_assets">

    </array>

Всё, что связано с министро — нам не интересно. MyLibWithQt — это с либа, использующая qt. Обратите внимание на libplugins_platforms_android_ Все плагины, которые идут дальше, должны содержать именно такое начало!!! Теперь, в манифесте вставляем следующий код:

<meta-data
                android:name="android.app.lib_name"
                android:value="MyLibWithQt " />
            <meta-data
                android:name="android.app.qt_sources_resource_id"
                android:resource="@array/qt_sources" />
            <meta-data
                android:name="android.app.repository"
                android:value="default" />
            <meta-data
                android:name="android.app.qt_libs_resource_id"
                android:resource="@array/qt_libs" />
            <meta-data
                android:name="android.app.bundled_libs_resource_id"
                android:resource="@array/bundled_libs" />
            <!-- Deploy Qt libs as part of package -->
            <meta-data
                android:name="android.app.bundle_local_qt_libs"
                android:value="1" />
            <meta-data
                android:name="android.app.bundled_in_lib_resource_id"
                android:resource="@array/bundled_in_lib" />
            <meta-data
                android:name="android.app.bundled_in_assets_resource_id"
                android:resource="@array/bundled_in_assets" />
            <!-- Run with local libs -->
            <meta-data
                android:name="android.app.use_local_qt_libs"
                android:value="1" />
            <meta-data
                android:name="android.app.libs_prefix"
                android:value="/data/local/tmp/qt/" />
            <meta-data
                android:name="android.app.load_local_libs"
                android:value="plugins/platforms/android/libqtforandroid.so" />
            <meta-data
                android:name="android.app.load_local_jars"
                android:value="jar/QtAndroid.jar:jar/QtAndroidAccessibility.jar:jar/QtAndroid-bundled.jar:jar/QtAndroidAccessibility-bundled.jar" />

Здесь мы указываем, какие либы надо ему загрузить из apk файла и как называется наша либа с qt. Разумеется, не забываем положить все эти плагины в папку jniLibs. Компилируем, запускаем, создаём в С коде
QApplication a(argc, argv);
и… получаем то же самое прокл знакомое исключение. Не создаётся наше QApplication.

Последняя напасть

Как же так? Вроде всё сделано правильно, а почему не работает? Всё оказалось… судите сами. Плагин libqtforandroid.so будет корректно загружен только в том случае, если в нашей библиотеке есть функция main. Если её нет, то один из jni методов в данном плагине не доработает до конца и приложение не будет создано! Поэтому, идём в нашу библиотеку, использующую qt, и определяем там функцию main. Логично, в ней и создать QApplication. Теперь всё, всё работает.

Резюме


Итак, подведём итоги. Чтобы работать с qt из Android studio нам необходимо:
1. Взять и видоизменить QtApplication (пара мест в коде) и определить свою QtActivity (чтобы вызвать свою разметку, setContentView вызываем позже, как создано будет QtApplication). Всё это добавить себе в код.
2. Настроить манифест, чтобы он подгружал библиотеки из apk.
3. В либу, в которой используется qt, добавить функцию main.
4. ??????
5. Profit!!!

Update Замечательный цикл статей про разработку для андроида на qt тут
Поделиться с друзьями
-->

Комментарии (12)


  1. Bimawa
    13.07.2016 09:39

    А скажите зачем это все?


    1. kolayuk
      13.07.2016 09:42

      Да мне тоже кажется автору было бы проще переехать в QtCreator, добавить свое приложение на Java в ANDROID_PACKAGE_SOURCE_DIR, а потом эту же папку через сгенерированные qt gradle-файлы подцепить в Android Studio. У нас сейчас проект так организован (он, правда большей частью на Qt) — все отлично работает.


      1. Rogvold91
        13.07.2016 09:45

        О, спасибо! Надо будет попробовать.

        upd. Я пробовал делать aar в qt creator, но если вкратце… там получилось правильно, но несколько противоречило куче кода, что был уже в андроид студии. Мне было лень переписывать этот момент и я решил разобраться что к чему.


    1. Rogvold91
      13.07.2016 09:45

      Ну… смотрите. У меня есть средних размеров Qt библиотека и приложение на Ios. Надо его портировать под андроид. Когда я не знал всех этих мук, я подумал:«Классно, есть андроид студия. На ней быстренько всё реализуем, а библиотеку подключу и буду пользоваться через native функции.» Сказано-сделано. Но тут засада: мне надо было в qt рисовать текст, а для этого неообходимо вызвать QApplication. Просто его вызов приводил к крашу приложения — поэтому вся эта кухня была затеяная ради того, чтобы вызвать QApplication и рисовать текст.

      ps. Я думаю посмотреть как будет себя вести вот это


      1. Bimawa
        13.07.2016 13:33

        Ок, спасибо за ответ.


  1. kolayuk
    13.07.2016 09:39

    Не очень понятно зачем костылировать QtActivity, почему бы не использовать наследование?


    1. Rogvold91
      13.07.2016 09:50

      Можно было подумать и об этом. Но у меня активити уже наследовала FragmentActivity (для гугл карт) — поэтому и решено было скопипастить.


  1. spise
    13.07.2016 11:51

    1. Rogvold91
      13.07.2016 12:46

      Now let’s see how to use Android Studio with Qt:

      Android Studio will be used only to:

      open the Gradle project.
      create, edit the java files.
      debug the Java part.

      Android Studio will NOT be used to run your Qt application, you still need to use Qt Creator for that job!

      А я как раз показываю как обойтись без qt creator. Более того, если смотреть его последнюю статью, то отчётливо видно, что он делает акцент на разработку из QtCreator, а мне хотелось, чтобы всё-таки акцент делался на андроид студию

      ps. Вообще, у него отличный этот цикл статей. Как раз с него изучение qt под андроид и начинал :)

      p.ps. Надо бы и о нём добавить


      1. spise
        13.07.2016 13:09

        Мне предложенный подход с разделением сред разработки (c++ + qt — Qt Creator, java (android) — Android Studio) показался вполне удобным. Правда, в основе проекта именно C++ и Qt, соответственно, основным инструментом является QtCreator. Java использую только по необходимости.


        1. Rogvold91
          13.07.2016 13:27

          Безусловно, если Java использовать по мере необходимости, а писать на плюсах, то лучше всё через Qt Creator. Ну а если наоборот Java сильно-сильно доминирует, то лучше, имхо, делать через андроид студию.


  1. Antervis
    13.07.2016 19:24

    когда Qt говорит «it could not find or load the Qt platform plugin» это обычно означает, что приложение не может найти нужную библиотеку в папке platforms проекта. Либо для этой библиотеки не разрешены зависимости