diff options
author | Christian Kandeler <christian.kandeler@qt.io> | 2018-08-16 14:15:28 +0200 |
---|---|---|
committer | Christian Kandeler <christian.kandeler@qt.io> | 2018-11-16 09:14:00 +0000 |
commit | 009f411f605d604f181b7652a6bbcc0d96831b42 (patch) | |
tree | a9474370340ced2fa4afaee34f42ac6375c7ed65 | |
parent | 970f59f322e0a0ee5915fc2443cd6bc38666631b (diff) |
Properly support building Qt apps for Android
... via the androiddeployqt tool.
Fixes: QBS-991
Change-Id: I4a3abe977fee6a9d1657a4fd6c1b43709429da9f
Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io>
13 files changed, 658 insertions, 14 deletions
diff --git a/doc/reference/modules/qt-android_support-module.qdoc b/doc/reference/modules/qt-android_support-module.qdoc new file mode 100644 index 000000000..f7ec595e3 --- /dev/null +++ b/doc/reference/modules/qt-android_support-module.qdoc @@ -0,0 +1,86 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qbs. +** +** $QT_BEGIN_LICENSE:FDL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \contentspage index.html + \qmltype Qt.android_support + \inqmlmodule QbsModules + \brief Provides Qt support for the Android platform. + + The \c Qt.android_support module provides the glue for \QBS' + Qt and Android support. + It is automatically pulled in by \c Qt.core, so you do not need to + add an explicit dependency to it in your product, unless you want + to set one of its properties. +*/ + +/*! + \qmlproperty bool Qt.android_support::useMinistro + + Whether or not to use the Ministro service. If this property is enabled, then + the Qt libraries required by your application as well as some other resources + will not be packaged into the APK file, but are expected to be present on the + device at run time. + + \defaultvalue \c false +*/ + +/*! + \qmlproperty string Qt.android_support::qmlRootDir + + The root directory of the product's QML files. This information is passed to + the \c androiddeployqt tool, which will use it to decide which resources to + include in the APK file. + + \defaultvalue \c product.sourceDirectory +*/ + +/*! + \qmlproperty stringList Qt.android_support::deploymentDependencies + + Use this property to completely override the Qt deployment dependencies + of your app. Corresponds to qmake's ANDROID_DEPLOYMENT_DEPENDENCIES. + + \defaultvalue \c undefined +*/ + +/*! + \qmlproperty stringList Qt.android_support::extraPlugins + + Additional non-asset files to be packaged. Corresponds to qmake's ANDROID_EXTRA_PLUGINS. + + \defaultvalue \c undefined +*/ + +/*! + \qmlproperty bool Qt.android_support::verboseAndroidDeployQt + + Enable this property if you want verbose output from the + \c androiddeployqt tool. + + \defaultvalue \c false +*/ diff --git a/share/qbs/modules/java/JavaModule.qbs b/share/qbs/modules/java/JavaModule.qbs index 1eba96d37..5191d6497 100644 --- a/share/qbs/modules/java/JavaModule.qbs +++ b/share/qbs/modules/java/JavaModule.qbs @@ -64,6 +64,8 @@ Module { property string keytoolFilePath: FileInfo.joinPaths(jdkPath, "bin", keytoolName) property string keytoolName: "keytool" + property bool _tagJniHeaders: true + property string jdkPath: jdk.path version: [compilerVersionMajor, compilerVersionMinor, compilerVersionPatch].join(".") @@ -232,9 +234,17 @@ Module { inputsFromDependencies: ["java.jar"] explicitlyDependsOn: ["java.class-internal"] - outputFileTags: ["java.class", "hpp"] // Annotations can produce additional java source files. Ignored for now. + outputFileTags: ["java.class"].concat(_tagJniHeaders ? ["hpp"] : []) // Annotations can produce additional java source files. Ignored for now. outputArtifacts: { - return JavaUtils.outputArtifacts(product, inputs); + var artifacts = JavaUtils.outputArtifacts(product, inputs); + if (!product.java._tagJniHeaders) { + for (var i = 0; i < artifacts.length; ++i) { + var a = artifacts[i]; + if (Array.isArray(a.fileTags)) + a.fileTags = a.fileTags.filter(function(tag) { return tag != "hpp"; }); + } + } + return artifacts; } prepare: { var cmd = new Command(ModUtils.moduleProperty(product, "compilerFilePath"), diff --git a/src/lib/qtprofilesetup/qtprofilesetup.cpp b/src/lib/qtprofilesetup/qtprofilesetup.cpp index 03bfc6cfd..f97b12f2c 100644 --- a/src/lib/qtprofilesetup/qtprofilesetup.cpp +++ b/src/lib/qtprofilesetup/qtprofilesetup.cpp @@ -656,6 +656,13 @@ static void createModules(Profile &profile, Settings *settings, copyTemplateFile(moduleTemplateFileName, qbsQtModuleDir, profile, qtEnvironment, &allFiles, &module); } + + // Note that it's not strictly necessary to copy this one, as it has no variable content. + // But we do it anyway for consistency (and it has no impact on the project files this way). + copyTemplateFile(QLatin1String("android_support.qbs"), + qbsQtModuleBaseDir + QLatin1String("/android_support"), profile, qtEnvironment, + &allFiles); + QDirIterator dit(qbsQtModuleBaseDir, QDirIterator::Subdirectories); while (dit.hasNext()) { dit.next(); diff --git a/src/lib/qtprofilesetup/templates.qrc b/src/lib/qtprofilesetup/templates.qrc index 18776b27a..35bddf8d7 100644 --- a/src/lib/qtprofilesetup/templates.qrc +++ b/src/lib/qtprofilesetup/templates.qrc @@ -17,5 +17,6 @@ <file>templates/quick.js</file> <file>templates/quick.qbs</file> <file>templates/plugin_support.qbs</file> + <file>templates/android_support.qbs</file> </qresource> </RCC> diff --git a/src/lib/qtprofilesetup/templates/android_support.qbs b/src/lib/qtprofilesetup/templates/android_support.qbs new file mode 100644 index 000000000..c4c4a51f0 --- /dev/null +++ b/src/lib/qtprofilesetup/templates/android_support.qbs @@ -0,0 +1,281 @@ +import qbs.File +import qbs.FileInfo +import qbs.ModUtils +import qbs.TextFile +import qbs.Utilities + +Module { + property bool useMinistro: false + property string qmlRootDir: product.sourceDirectory + property stringList extraPrefixDirs + property stringList deploymentDependencies // qmake: ANDROID_DEPLOYMENT_DEPENDENCIES + property stringList extraPlugins // qmake: ANDROID_EXTRA_PLUGINS + property bool verboseAndroidDeployQt: false + + property string _androidDeployQtFilePath: FileInfo.joinPaths(_qtInstallDir, "bin", + "androiddeployqt") + property string _qtInstallDir + property bool _enableSdkSupport: product.type && product.type.contains("android.apk") + && !consoleApplication + property bool _enableNdkSupport: !product.aggregate || product.multiplexConfigurationId + property string _templatesBaseDir: FileInfo.joinPaths(_qtInstallDir, "src", "android") + property string _deployQtOutDir: FileInfo.joinPaths(product.buildDirectory, "deployqt_out") + + Depends { name: "Android.sdk"; condition: _enableSdkSupport } + Depends { name: "Android.ndk"; condition: _enableNdkSupport } + Depends { name: "java"; condition: _enableSdkSupport } + + Properties { + condition: _enableNdkSupport && qbs.toolchain.contains("clang") + Android.ndk.appStl: "c++_shared" + } + Properties { + condition: _enableNdkSupport && !qbs.toolchain.contains("clang") + Android.ndk.appStl: "gnustl_shared" + } + Properties { + condition: _enableSdkSupport + Android.sdk.customManifestProcessing: true + java._tagJniHeaders: false // prevent rule cycle + } + + Rule { + condition: _enableSdkSupport + multiplex: true + property stringList inputTags: "android.nativelibrary" + inputsFromDependencies: inputTags + inputs: product.aggregate ? [] : inputTags + Artifact { + filePath: "androiddeployqt.json" + fileTags: "qt_androiddeployqt_input" + } + prepare: { + var cmd = new JavaScriptCommand(); + cmd.description = "creating " + output.fileName; + cmd.sourceCode = function() { + var theBinary; + var nativeLibs = inputs["android.nativelibrary"]; + if (nativeLibs.length === 1) { + theBinary = nativeLibs[0]; + } else { + for (i = 0; i < nativeLibs.length; ++i) { + var candidate = nativeLibs[i]; + if (candidate.fileName.contains(candidate.product.targetName)) { + if (theBinary) { + throw "Qt applications for Android support only one native binary " + + "per package.\n" + + "In particular, you cannot build a Qt app for more than " + + "one architecture at the same time."; + } + theBinary = candidate; + } + } + } + var f = new TextFile(output.filePath, TextFile.WriteOnly); + f.writeLine("{"); + f.writeLine('"description": "This file was generated by qbs to be read by ' + + 'androiddeployqt and should not be modified by hand.",'); + f.writeLine('"qt": "' + product.Qt.android_support._qtInstallDir + '",'); + f.writeLine('"sdk": "' + product.Android.sdk.sdkDir + '",'); + f.writeLine('"sdkBuildToolsRevision": "' + product.Android.sdk.buildToolsVersion + + '",'); + f.writeLine('"ndk": "' + product.Android.sdk.ndkDir + '",'); + var toolPrefix = theBinary.cpp.toolchainTriple; + var toolchainPrefix = toolPrefix.startsWith("i686-") ? "x86" : toolPrefix; + f.writeLine('"toolchain-prefix": "' + toolchainPrefix + '",'); + f.writeLine('"tool-prefix": "' + toolPrefix + '",'); + f.writeLine('"toolchain-version": "' + theBinary.Android.ndk.toolchainVersion + + '",'); + f.writeLine('"ndk-host": "' + theBinary.Android.ndk.hostArch + '",'); + f.writeLine('"target-architecture": "' + theBinary.Android.ndk.abi + '",'); + f.writeLine('"qml-root-path": "' + product.Qt.android_support.qmlRootDir + '",'); + var deploymentDeps = product.Qt.android_support.deploymentDependencies; + if (deploymentDeps && deploymentDeps.length > 0) + f.writeLine('"deployment-dependencies": "' + deploymentDeps.join() + '",'); + var extraPlugins = product.Qt.android_support.extraPlugins; + if (extraPlugins && extraPlugins.length > 0) + f.writeLine('"android-extra-plugins": "' + extraPlugins.join() + '",'); + var prefixDirs = product.Qt.android_support.extraPrefixDirs; + if (prefixDirs && prefixDirs.length > 0) + f.writeLine('"extraPrefixDirs": ' + JSON.stringify(prefixDirs) + ','); + if (Array.isArray(product.qmlImportPaths) && product.qmlImportPaths.length > 0) + f.writeLine('"qml-import-paths": "' + product.qmlImportPaths.join(',') + '",'); + f.writeLine('"application-binary": "' + theBinary.filePath + '"'); + f.writeLine("}"); + f.close(); + }; + return cmd; + } + } + + // We use the manifest template from the Qt installation if and only if the project + // does not provide a manifest file. + Rule { + condition: _enableSdkSupport + multiplex: true + requiresInputs: false + inputs: "android.manifest" + excludedInputs: "qt.android_manifest" + outputFileTags: ["android.manifest", "qt.android_manifest"] + outputArtifacts: { + if (inputs["android.manifest"]) + return []; + return [{ + filePath: "qt_manifest/AndroidManifest.xml", + fileTags: ["android.manifest", "qt.android_manifest"] + }]; + } + prepare: { + var cmd = new JavaScriptCommand(); + cmd.description = "copying Qt Android manifest template"; + cmd.sourceCode = function() { + File.copy(FileInfo.joinPaths(product.Qt.android_support._templatesBaseDir, + "templates", "AndroidManifest.xml"), output.filePath); + }; + return cmd; + } + } + + Rule { + condition: _enableSdkSupport + multiplex: true + inputs: ["qt_androiddeployqt_input", "android.manifest_processed"] + outputFileTags: [ + "android.manifest_final", "android.resources", "android.assets", "bundled_jar", + "android.deployqt_list", + ] + outputArtifacts: { + var artifacts = [ + { + filePath: "AndroidManifest.xml", + fileTags: "android.manifest_final" + }, + { + filePath: product.Qt.android_support._deployQtOutDir + "/res/values/libs.xml", + fileTags: "android.resources" + }, + { + filePath: product.Qt.android_support._deployQtOutDir + + "/res/values/strings.xml", + fileTags: "android.resources" + }, + { + filePath: product.Qt.android_support._deployQtOutDir + "/assets/.dummy", + fileTags: "android.assets" + }, + { + filePath: "deployqt.list", + fileTags: "android.deployqt_list" + }, + + ]; + if (!product.Qt.android_support.useMinistro) { + artifacts.push({ + filePath: FileInfo.joinPaths(product.java.classFilesDir, "QtAndroid.jar"), + fileTags: ["bundled_jar"] + }); + } + return artifacts; + } + prepare: { + var copyCmd = new JavaScriptCommand(); + copyCmd.description = "copying Qt resource templates"; + copyCmd.sourceCode = function() { + File.copy(inputs["android.manifest_processed"][0].filePath, + product.Qt.android_support._deployQtOutDir + "/AndroidManifest.xml"); + File.copy(product.Qt.android_support._templatesBaseDir + "/java/res", + product.Qt.android_support._deployQtOutDir + "/res"); + File.copy(product.Qt.android_support._templatesBaseDir + + "/templates/res/values/libs.xml", + product.Qt.android_support._deployQtOutDir + "/res/values/libs.xml"); + try { + File.remove(FileInfo.path(outputs["android.assets"][0].filePath)); + } catch (e) { + } + }; + var androidDeployQtArgs = [ + "--output", product.Qt.android_support._deployQtOutDir, + "--input", inputs["qt_androiddeployqt_input"][0].filePath, "--aux-mode", + "--deployment", product.Qt.android_support.useMinistro ? "ministro" : "bundled", + "--android-platform", product.Android.sdk.platform, + ]; + if (product.Qt.android_support.verboseAndroidDeployQt) + args.push("--verbose"); + var androidDeployQtCmd = new Command( + product.Qt.android_support._androidDeployQtFilePath, androidDeployQtArgs); + androidDeployQtCmd.description = "running androiddeployqt"; + + // We do not want androiddeployqt to write directly into our APK base dir, so + // we ran it on an isolated directory and now we move stuff over. + // We remember the files for which we do that, so if the next invocation + // of androiddeployqt creates fewer files, the other ones are removed from + // the APK base dir. + var moveCmd = new JavaScriptCommand(); + moveCmd.description = "processing androiddeployqt outout"; + moveCmd.sourceCode = function() { + File.move(product.Qt.android_support._deployQtOutDir + "/AndroidManifest.xml", + outputs["android.manifest_final"][0].filePath); + var libsDir = product.Qt.android_support._deployQtOutDir + "/libs"; + var libDir = product.Android.sdk.apkContentsDir + "/lib"; + var listFilePath = outputs["android.deployqt_list"][0].filePath; + var oldLibs = []; + try { + var listFile = new TextFile(listFilePath, TextFile.ReadOnly); + var listFileLine = listFile.readLine(); + while (listFileLine) { + oldLibs.push(listFileLine); + listFileLine = listFile.readLine(); + } + listFile.close(); + } catch (e) { + } + listFile = new TextFile(listFilePath, TextFile.WriteOnly); + var newLibs = []; + var moveLibFiles = function(prefix) { + var fullSrcPrefix = FileInfo.joinPaths(libsDir, prefix); + var files = File.directoryEntries(fullSrcPrefix, File.Files); + for (var i = 0; i < files.length; ++i) { + var file = files[i]; + var srcFilePath = FileInfo.joinPaths(fullSrcPrefix, file); + var targetFilePath; + if (file.endsWith(".jar")) + targetFilePath = FileInfo.joinPaths(product.java.classFilesDir, file); + else + targetFilePath = FileInfo.joinPaths(libDir, prefix, file); + File.move(srcFilePath, targetFilePath); + listFile.writeLine(targetFilePath); + newLibs.push(targetFilePath); + } + var dirs = File.directoryEntries(fullSrcPrefix, + File.Dirs | File.NoDotAndDotDot); + for (i = 0; i < dirs.length; ++i) + moveLibFiles(FileInfo.joinPaths(prefix, dirs[i])); + }; + moveLibFiles(""); + listFile.close(); + for (i = 0; i < oldLibs.length; ++i) { + if (!newLibs.contains(oldLibs[i])) + File.remove(oldLibs[i]); + } + }; + return [copyCmd, androidDeployQtCmd, moveCmd]; + } + } + + Group { + condition: Qt.android_support._enableSdkSupport + name: "helper sources from qt" + prefix: Qt.android_support._templatesBaseDir + "/java/" + Android.sdk.aidlSearchPaths: prefix + "src" + files: [ + "**/*.java", + "**/*.aidl", + ] + } + + validate: { + if (Utilities.versionCompare(version, "5.12") < 0) + throw ModUtils.ModuleError("Cannot use Qt " + version + " with Android. " + + "Version 5.12 or later is required."); + } +} diff --git a/src/lib/qtprofilesetup/templates/core.qbs b/src/lib/qtprofilesetup/templates/core.qbs index 7f7329501..1a83aab71 100644 --- a/src/lib/qtprofilesetup/templates/core.qbs +++ b/src/lib/qtprofilesetup/templates/core.qbs @@ -18,6 +18,13 @@ Module { Depends { name: "cpp" } + Depends { name: "Qt.android_support"; condition: qbs.targetOS.contains("android") } + Properties { + condition: qbs.targetOS.contains("android") + Qt.android_support._qtInstallDir: FileInfo.path(binPath) + Qt.android_support.version: version + } + version: @version@ property stringList architectures: @archs@ property string targetPlatform: @targetPlatform@ diff --git a/tests/auto/blackbox/testdata-android/qml-app/main.cpp b/tests/auto/blackbox/testdata-android/qml-app/main.cpp new file mode 100644 index 000000000..e7cf5e16e --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/main.cpp @@ -0,0 +1,21 @@ +#include <QGuiApplication> +#include <QQmlApplicationEngine> + +int main(int argc, char *argv[]) +{ + if (qEnvironmentVariableIsEmpty("QTGLESSTREAM_DISPLAY")) { + qputenv("QT_QPA_EGLFS_PHYSICAL_WIDTH", QByteArray("213")); + qputenv("QT_QPA_EGLFS_PHYSICAL_HEIGHT", QByteArray("120")); + + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + } + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); + if (engine.rootObjects().isEmpty()) + return -1; + + return app.exec(); +} diff --git a/tests/auto/blackbox/testdata-android/qml-app/main.qml b/tests/auto/blackbox/testdata-android/qml-app/main.qml new file mode 100644 index 000000000..45ee20a2f --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/main.qml @@ -0,0 +1,9 @@ +import QtQuick 2.6 +import QtQuick.Window 2.2 + +Window { + visible: true + width: 640 + height: 480 + title: qsTr("Hello World") +} diff --git a/tests/auto/blackbox/testdata-android/qml-app/qml-app.qbs b/tests/auto/blackbox/testdata-android/qml-app/qml-app.qbs new file mode 100644 index 000000000..56b9d6eaf --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/qml-app.qbs @@ -0,0 +1,14 @@ +QtApplication { + name: "qmlapp" + Depends { name: "Qt.quick" } + Depends { name: "Qt.android_support" } + Properties { + condition: qbs.targetOS.contains("android") + Qt.android_support.extraPrefixDirs: path + } + property stringList qmlImportPaths: path + files: [ + "main.cpp", + "qml.qrc", + ] +} diff --git a/tests/auto/blackbox/testdata-android/qml-app/qml.qrc b/tests/auto/blackbox/testdata-android/qml-app/qml.qrc new file mode 100644 index 000000000..5f6483ac3 --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/qml.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>main.qml</file> + </qresource> +</RCC> diff --git a/tests/auto/blackbox/testdata-android/qml-app/src/main/AndroidManifest.xml b/tests/auto/blackbox/testdata-android/qml-app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..066ec0a63 --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/src/main/AndroidManifest.xml @@ -0,0 +1,81 @@ +<?xml version='1.0' encoding='utf-8'?> +<manifest package="org.qtproject.example" xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="1.0" android:versionCode="1" android:installLocation="auto"> + <application android:hardwareAccelerated="true" android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="-- %%INSERT_APP_NAME%% --"> + <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" + android:name="org.qtproject.qt5.android.bindings.QtActivity" + android:label="-- %%INSERT_APP_NAME%% --" + android:screenOrientation="unspecified" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + + <!-- Application arguments --> + <!-- meta-data android:name="android.app.arguments" android:value="arg1 arg2 arg3"/ --> + <!-- Application arguments --> + + <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/> + <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="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/> + <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="-- %%USE_LOCAL_QT_LIBS%% --"/> + <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="-- %%INSERT_LOCAL_LIBS%% --"/> + <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/> + <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/> + <!-- Messages maps --> + <meta-data android:value="@string/ministro_not_found_msg" android:name="android.app.ministro_not_found_msg"/> + <meta-data android:value="@string/ministro_needed_msg" android:name="android.app.ministro_needed_msg"/> + <meta-data android:value="@string/fatal_error_msg" android:name="android.app.fatal_error_msg"/> + <!-- Messages maps --> + + <!-- Splash screen --> + <!-- meta-data android:name="android.app.splash_screen_drawable" android:resource="@drawable/logo"/ --> + <!-- meta-data android:name="android.app.splash_screen_sticky" android:value="true"/ --> + <!-- Splash screen --> + + <!-- Background running --> + <!-- Warning: changing this value to true may cause unexpected crashes if the + application still try to draw after + "applicationStateChanged(Qt::ApplicationSuspended)" + signal is sent! --> + <meta-data android:name="android.app.background_running" android:value="false"/> + <!-- Background running --> + + <!-- auto screen scale factor --> + <meta-data android:name="android.app.auto_screen_scale_factor" android:value="false"/> + <!-- auto screen scale factor --> + + <!-- extract android style --> + <!-- available android:values : + * full - useful QWidget & Quick Controls 1 apps + * minimal - useful for Quick Controls 2 apps, it is much faster than "full" + * none - useful for apps that don't use any of the above Qt modules + --> + <meta-data android:name="android.app.extract_android_style" android:value="full"/> + <!-- extract android style --> + </activity> + + <!-- For adding service(s) please check: https://wiki.qt.io/AndroidServices --> + + </application> + + <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="16"/> + <supports-screens android:largeScreens="true" android:normalScreens="true" android:anyDensity="true" android:smallScreens="true"/> + + <!-- The following comment will be replaced upon deployment with default permissions based on the dependencies of the application. + Remove the comment if you do not require these default permissions. --> + <!-- %%INSERT_PERMISSIONS --> + + <!-- The following comment will be replaced upon deployment with default features based on the dependencies of the application. + Remove the comment if you do not require these default features. --> + <!-- %%INSERT_FEATURES --> + +</manifest> diff --git a/tests/auto/blackbox/testdata-android/qml-app/src/main/assets/dummyasset.txt b/tests/auto/blackbox/testdata-android/qml-app/src/main/assets/dummyasset.txt new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/auto/blackbox/testdata-android/qml-app/src/main/assets/dummyasset.txt diff --git a/tests/auto/blackbox/tst_blackboxandroid.cpp b/tests/auto/blackbox/tst_blackboxandroid.cpp index 5630bdcb5..bea4ee595 100644 --- a/tests/auto/blackbox/tst_blackboxandroid.cpp +++ b/tests/auto/blackbox/tst_blackboxandroid.cpp @@ -69,6 +69,11 @@ TestBlackboxAndroid::TestBlackboxAndroid() { } +static QString theProfileName(bool forQt) +{ + return forQt ? "qbs_autotests-android-qt" : profileName(); +} + void TestBlackboxAndroid::android() { QFETCH(QString, projectDir); @@ -77,7 +82,7 @@ void TestBlackboxAndroid::android() QFETCH(QStringList, customProperties); const SettingsPtr s = settings(); - Profile p(profileName(), s.get()); + Profile p(theProfileName(projectDir == "qml-app"), s.get()); int status; const auto androidPaths = findAndroid(&status, p.name()); QCOMPARE(status, 0); @@ -112,7 +117,10 @@ void TestBlackboxAndroid::android() buildParams.profile = p.name(); QCOMPARE(runQbs(buildParams), 0); for (const QString &productName : qAsConst(productNames)) { - QCOMPARE(m_qbsStdout.count("Generating BuildConfig.java"), productNames.size()); + const QByteArray tag(QTest::currentDataTag()); + const bool isIncrementalBuild = tag.startsWith("qml app") && tag != "qml app"; + QCOMPARE(m_qbsStdout.count("Generating BuildConfig.java"), + isIncrementalBuild ? 0 : productNames.size()); QVERIFY(m_qbsStdout.contains(productName.toLocal8Bit() + ".apk")); const QString apkFilePath = relativeProductBuildDir(productName, configName) + '/' + productName + ".apk"; @@ -146,10 +154,15 @@ void TestBlackboxAndroid::android() auto isFileSharedObject = [](const QByteArray &f) { return f.endsWith(".so"); }; - if (none_of(actualFiles, isFileSharedObject)) + const auto isQmlToolingLib = [](const QByteArray &f) { + return f.contains("qmltooling"); + }; + if (none_of(actualFiles, isFileSharedObject) + || std::all_of(actualFiles.cbegin(), actualFiles.cend(), isQmlToolingLib)) { QWARN(msg); - else + } else { QFAIL(msg); + } } } @@ -176,6 +189,7 @@ void TestBlackboxAndroid::android_data() { const SettingsPtr s = settings(); const Profile p(profileName(), s.get()); + const Profile pQt(theProfileName(true), s.get()); QStringList archsStringList = p.value(QLatin1String("qbs.architectures")).toStringList(); if (archsStringList.empty()) archsStringList << QStringLiteral("armv7a"); // must match default in common.qbs @@ -186,10 +200,19 @@ void TestBlackboxAndroid::android_data() .replace("armv5te", "armeabi") .replace("arm64", "arm64-v8a"); }); - const bool usesClang = p.value(QLatin1String("qbs.toolchainType")).toString() == "clang"; - const auto cxxLibPath = [usesClang](const QByteArray &oldcxxLib) { + const auto cxxLibPath = [&p, &pQt](const QByteArray &oldcxxLib, bool forQt) { + const bool usesClang = (forQt ? pQt : p).value(QLatin1String("qbs.toolchainType")) + .toString() == "clang"; return QByteArray("lib/${ARCH}/") + (usesClang ? "libc++_shared.so" : oldcxxLib); }; + const QByteArrayList archsForQt = { pQt.value("qbs.architecture").toString().toUtf8() }; + QByteArrayList ndkArchsForQt = archsForQt; + if (ndkArchsForQt.first() == "armv7a") + ndkArchsForQt.first() = "armeabi-v7a"; + else if (ndkArchsForQt.first() == "armv5te") + ndkArchsForQt.first() = "armeabi"; + else if (ndkArchsForQt.first() == "arm64") + ndkArchsForQt.first() = "arm64-v8a"; auto expandArchs = [] (const QByteArrayList &archs, const QByteArrayList &lst) { const QByteArray &archPlaceHolder = "${ARCH}"; @@ -221,7 +244,7 @@ void TestBlackboxAndroid::android_data() "assets/Shaders/ShaderPlain.fsh", "assets/Shaders/VS_ShaderPlain.vsh", "lib/${ARCH}/libgdbserver.so", - cxxLibPath("libgnustl_shared.so"), + cxxLibPath("libgnustl_shared.so", false), "lib/${ARCH}/libTeapotNativeActivity.so", "res/layout/widgets.xml"})) << QStringList(); @@ -229,10 +252,109 @@ void TestBlackboxAndroid::android_data() << "minimal-native" << QStringList("minimalnative") << (QList<QByteArrayList>() << commonFiles + expandArchs({archs.first()}, { "lib/${ARCH}/libminimalnative.so", - cxxLibPath("libstlport_shared.so"), + cxxLibPath("libstlport_shared.so", false), "lib/${ARCH}/libdependency.so"})) << QStringList{"products.minimalnative.multiplexByQbsProperties:[]", "modules.qbs.architecture:" + archsStringList.first()}; + QTest::newRow("qml app") + << "qml-app" << QStringList("qmlapp") + << (QList<QByteArrayList>() << commonFiles + expandArchs(ndkArchsForQt, { + "resources.arsc", + "assets/--Added-by-androiddeployqt--/qml/QtQuick.2/plugins.qmltypes", + "assets/--Added-by-androiddeployqt--/qml/QtQuick.2/qmldir", + "assets/--Added-by-androiddeployqt--/qml/QtQuick/Window.2/plugins.qmltypes", + "assets/--Added-by-androiddeployqt--/qml/QtQuick/Window.2/qmldir", + "assets/--Added-by-androiddeployqt--/qt_cache_pregenerated_file_list", + "lib/${ARCH}/libgdbserver.so", + cxxLibPath("libgnustl_shared.so", true), + "lib/${ARCH}/libplugins_bearer_libqandroidbearer.so", + "lib/${ARCH}/libplugins_imageformats_libqgif.so", + "lib/${ARCH}/libplugins_imageformats_libqicns.so", + "lib/${ARCH}/libplugins_imageformats_libqico.so", + "lib/${ARCH}/libplugins_imageformats_libqjpeg.so", + "lib/${ARCH}/libplugins_imageformats_libqtga.so", + "lib/${ARCH}/libplugins_imageformats_libqtiff.so", + "lib/${ARCH}/libplugins_imageformats_libqwbmp.so", + "lib/${ARCH}/libplugins_imageformats_libqwebp.so", + "lib/${ARCH}/libplugins_platforms_android_libqtforandroid.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_debugger.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_inspector.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_local.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_messages.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_native.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_nativedebugger.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_profiler.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_preview.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_quickprofiler.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_server.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_tcp.so", + "lib/${ARCH}/libqml_QtQuick.2_libqtquick2plugin.so", + "lib/${ARCH}/libqml_QtQuick_Window.2_libwindowplugin.so", + "lib/${ARCH}/libQt5Core.so", + "lib/${ARCH}/libQt5Gui.so", + "lib/${ARCH}/libQt5Network.so", + "lib/${ARCH}/libQt5Qml.so", + "lib/${ARCH}/libQt5QuickParticles.so", + "lib/${ARCH}/libQt5Quick.so", + "lib/${ARCH}/libqmlapp.so", + "res/layout/splash.xml"})) + << QStringList{"modules.Android.sdk.automaticSources:false", + "modules.qbs.architecture:" + archsForQt.first()}; + QTest::newRow("qml app using Ministro") + << "qml-app" << QStringList("qmlapp") + << (QList<QByteArrayList>() << commonFiles + expandArchs(ndkArchsForQt, { + "resources.arsc", + "assets/--Added-by-androiddeployqt--/qt_cache_pregenerated_file_list", + "lib/${ARCH}/libgdbserver.so", + cxxLibPath("libgnustl_shared.so", true), + "lib/${ARCH}/libqmlapp.so", + "res/layout/splash.xml"})) + << QStringList{"modules.Qt.android_support.useMinistro:true", + "modules.Android.sdk.automaticSources:false"}; + QTest::newRow("qml app with custom metadata") + << "qml-app" << QStringList("qmlapp") + << (QList<QByteArrayList>() << commonFiles + expandArchs(ndkArchsForQt, { + "resources.arsc", + "assets/--Added-by-androiddeployqt--/qml/QtQuick.2/plugins.qmltypes", + "assets/--Added-by-androiddeployqt--/qml/QtQuick.2/qmldir", + "assets/--Added-by-androiddeployqt--/qml/QtQuick/Window.2/plugins.qmltypes", + "assets/--Added-by-androiddeployqt--/qml/QtQuick/Window.2/qmldir", + "assets/--Added-by-androiddeployqt--/qt_cache_pregenerated_file_list", + "assets/dummyasset.txt", + "lib/${ARCH}/libgdbserver.so", + cxxLibPath("libgnustl_shared.so", true), + "lib/${ARCH}/libplugins_bearer_libqandroidbearer.so", + "lib/${ARCH}/libplugins_imageformats_libqgif.so", + "lib/${ARCH}/libplugins_imageformats_libqicns.so", + "lib/${ARCH}/libplugins_imageformats_libqico.so", + "lib/${ARCH}/libplugins_imageformats_libqjpeg.so", + "lib/${ARCH}/libplugins_imageformats_libqtga.so", + "lib/${ARCH}/libplugins_imageformats_libqtiff.so", + "lib/${ARCH}/libplugins_imageformats_libqwbmp.so", + "lib/${ARCH}/libplugins_imageformats_libqwebp.so", + "lib/${ARCH}/libplugins_platforms_android_libqtforandroid.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_debugger.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_inspector.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_local.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_messages.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_native.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_nativedebugger.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_profiler.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_preview.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_quickprofiler.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_server.so", + "lib/${ARCH}/libplugins_qmltooling_libqmldbg_tcp.so", + "lib/${ARCH}/libqml_QtQuick.2_libqtquick2plugin.so", + "lib/${ARCH}/libqml_QtQuick_Window.2_libwindowplugin.so", + "lib/${ARCH}/libQt5Core.so", + "lib/${ARCH}/libQt5Gui.so", + "lib/${ARCH}/libQt5Network.so", + "lib/${ARCH}/libQt5Qml.so", + "lib/${ARCH}/libQt5QuickParticles.so", + "lib/${ARCH}/libQt5Quick.so", + "lib/${ARCH}/libqmlapp.so", + "res/layout/splash.xml"})) + << QStringList("modules.Android.sdk.automaticSources:true"); QTest::newRow("no native") << "no-native" << QStringList("com.example.android.basicmediadecoder") @@ -265,24 +387,24 @@ void TestBlackboxAndroid::android_data() "lib/${ARCH}/libgdbserver.so", "lib/${ARCH}/liblib1.so", "lib/${ARCH}/liblib2.so", - cxxLibPath("libstlport_shared.so")})) + cxxLibPath("libstlport_shared.so", false)})) << QStringList(); QByteArrayList expectedFiles1 = (commonFiles + expandArchs(QByteArrayList{"armeabi-v7a", "x86"}, { "resources.arsc", "lib/${ARCH}/libgdbserver.so", "lib/${ARCH}/libp1lib1.so", - cxxLibPath("libstlport_shared.so")}) + cxxLibPath("libstlport_shared.so", false)}) + expandArchs(QByteArrayList{archs}, { "resources.arsc", "lib/${ARCH}/libgdbserver.so", "lib/${ARCH}/libp1lib2.so", - cxxLibPath("libstlport_shared.so")})).toSet().toList(); + cxxLibPath("libstlport_shared.so", false)})).toSet().toList(); QByteArrayList expectedFiles2 = commonFiles + expandArchs(archs, { "lib/${ARCH}/libgdbserver.so", "lib/${ARCH}/libp2lib1.so", "lib/${ARCH}/libp2lib2.so", - cxxLibPath("libstlport_shared.so")}); + cxxLibPath("libstlport_shared.so", false)}); QTest::newRow("multiple apks") << "multiple-apks-per-project" << (QStringList() << "twolibs1" << "twolibs2") |