Pull to refresh

Разработка Android приложений с использованием qt и android studio часть вторая

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

Недавно я на эмоциях опубликовал статью, где я рассказывал о всех своих злоключениях при попытки создать 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 тут
Tags:
Hubs:
Total votes 14: ↑11 and ↓3+8
Comments12

Articles