Android: support uncompressed native libs within APKs

Android 6 and above produces uncompressed native libraries that
are only part of the APK by default. However, Qt had this behavior
explicitly disabled via packagingOptions.jniLibs.useLegacyPackaging
Gradle flag (previously extractNativeLibs manifest flag) because
we didn't support loading libraries directly from the APK.

This patch adds support for reading and loading shared libraries
directly from the APK without having them extracted to disk. Enabling
this might increase slightly the total size of produced APKs, but
saves on disk space after installation and also on update sizes from
the Play Store and slightly faster startups [1][2].

Loading libraries on the Java side is handled by System.loadLibrary().
On C++, dlopen(3) can directly handle library paths relative to the
APK file [3] which would save us the need to add custom code that calls
android_dlopen_ext() [4] which works with compressed libraries then
using AssetFileDescriptor and having to manage its file descriptor
manually.

To ensure proper integration with various Qt APIs and modules, this
adds a QAbstractFileEngine/Iterator implementations to allow reading
and listing APK files. Since, the files are expected to not change,
they are cached once at startup and re-used thereafter. The engine
implementation allows reading the libraries content using Android's
AssetManager. Also, it allows mapping the libraries directly to
memory to allow proper integration with QPluginLoader.

For plugins, the native libs dir inside the APK is added to Qt and
QML plugins search paths.

With this patch, both compressed and uncompressed libs should work,
to ensure this, an auto test is added with 'useLegacyPackaging true'
to make sure both scenarios still works.

[ChangeLog][Android] Add support for uncompressed native libraries
within APKs.

[1] https://android-developers.googleblog.com/2016/07/improvements-for-
smaller-app-downloads.html
[2] https://developer.android.com/guide/topics/manifest/application-
element#extractNativeLibs
[3] https://android.googlesource.com/platform/bionic/+/master/android-
changes-for-ndk-developers.md#Opening-shared-libraries-directly-from-an-
APK
[4] https://developer.android.com/ndk/reference/group/
libdl#android_dlopen_ext

Fixes: QTBUG-61072
Fixes: QTBUG-97650
Change-Id: Ica6c4cc9e5bd8f3610829b76b64bf599339435d9
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Assam Boudjelthia 2024-08-30 17:01:42 +03:00
parent 6543f50536
commit 0db5b424cd
24 changed files with 852 additions and 36 deletions

View File

@ -51,6 +51,7 @@ set(java_sources
src/org/qtproject/qt/android/QtAbstractListModel.java
src/org/qtproject/qt/android/QtSignalListener.java
src/org/qtproject/qt/android/BackgroundActionsTracker.java
src/org/qtproject/qt/android/QtApkFileEngine.java
)
qt_internal_add_jar(Qt${QtBase_VERSION_MAJOR}Android

View File

@ -0,0 +1,213 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
package org.qtproject.qt.android;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.pm.PackageManager;
import java.io.FileInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.NoSuchElementException;
import java.util.zip.ZipFile;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Collections;
import java.util.Comparator;
import android.util.Log;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.MappedByteBuffer;
import java.nio.ByteOrder;
@UsedFromNativeCode
class QtApkFileEngine {
private final static String QtTAG = QtApkFileEngine.class.getSimpleName();
private static String m_appApkPath;
private AssetFileDescriptor m_assetFd;
private AssetManager m_assetManager;
private FileInputStream m_assetInputStream;
private long m_pos = -1;
QtApkFileEngine(Context context)
{
m_assetManager = context.getAssets();
}
boolean open(String fileName)
{
try {
m_assetFd = m_assetManager.openNonAssetFd(fileName);
m_assetInputStream = m_assetFd.createInputStream();
} catch (IOException e) {
Log.e(QtTAG, "Failed to open the app APK with " + e.toString());
}
return m_assetInputStream != null;
}
boolean close()
{
try {
if (m_assetInputStream != null)
m_assetInputStream.close();
if (m_assetFd != null)
m_assetFd.close();
} catch (IOException e) {
Log.e(QtTAG, "Failed to close resources with " + e.toString());
}
return m_assetInputStream == null && m_assetFd == null;
}
long pos()
{
return m_pos;
}
boolean seek(int pos)
{
if (m_assetInputStream != null && m_assetInputStream.markSupported()) {
try {
m_assetInputStream.mark(pos);
m_assetInputStream.reset();
m_pos = pos;
return true;
} catch (IOException ignored) { }
}
return false;
}
MappedByteBuffer getMappedByteBuffer(long offset, long size)
{
try {
FileChannel fileChannel = m_assetInputStream.getChannel();
long position = fileChannel.position() + offset;
MappedByteBuffer mapped = fileChannel.map(MapMode.READ_ONLY, position, size);
mapped.order(ByteOrder.LITTLE_ENDIAN);
fileChannel.close();
return mapped;
} catch (Exception e) {
Log.e(QtTAG, "Failed to map APK file to memory with " + e.toString());
}
return null;
}
byte[] read(long maxlen)
{
if (m_assetInputStream == null)
return null;
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
int bytesRead;
int totalBytesRead = 0;
byte[] buffer = new byte[1024];
try {
while (totalBytesRead < maxlen) {
int remainingBytes = (int) maxlen - totalBytesRead;
int bytesToRead = Math.min(buffer.length, remainingBytes);
if ((bytesRead = m_assetInputStream.read(buffer, 0, bytesToRead)) == -1)
break;
outputStream.write(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
}
outputStream.close();
} catch (IOException e) {
Log.e(QtTAG, "Failed to read content with " + e.toString());
}
return outputStream.toByteArray();
}
static String getAppApkFilePath()
{
if (m_appApkPath != null)
return m_appApkPath;
try {
Context context = QtNative.getContext();
PackageManager pm = context.getPackageManager();
m_appApkPath = pm.getApplicationInfo(context.getPackageName(), 0).sourceDir;
} catch (PackageManager.NameNotFoundException e) {
Log.e(QtTAG, "Failed to get the app APK path with " + e.toString());
return null;
}
return m_appApkPath;
}
static class JFileInfo
{
String relativePath;
boolean isDir;
long size;
}
static ArrayList<JFileInfo> getApkFileInfos(String apkPath)
{
ArrayList<JFileInfo> fileInfos = new ArrayList<>();
HashSet<String> dirSet = new HashSet<>();
HashSet<String> allDirsSet = new HashSet<>();
try (ZipFile zipFile = new ZipFile(apkPath)) {
Enumeration<? extends ZipEntry> enumerator = zipFile.entries();
while (enumerator.hasMoreElements()) {
ZipEntry entry = enumerator.nextElement();
String name = entry.getName();
// Limit the listing to lib directory
if (name.startsWith("lib/")) {
JFileInfo info = new JFileInfo();
info.relativePath = name;
info.isDir = entry.isDirectory();
info.size = entry.getSize();
fileInfos.add(info);
// check directories
dirSet.add(name.substring(0, name.lastIndexOf("/") + 1));
}
}
// ZipFile iterator doesn't seem to add directories, so add them manually.
for (String path : dirSet) {
int index = 0;
while ((index = path.indexOf("/", index + 1)) != -1) {
String dir = path.substring(0, index);
allDirsSet.add(dir);
}
}
for (String dir : allDirsSet) {
JFileInfo info = new JFileInfo();
info.relativePath = dir;
info.isDir = true;
info.size = -1;
fileInfos.add(info);
}
// sort alphabetically based on the file path
Collections.sort(fileInfos, new Comparator<JFileInfo>() {
@Override
public int compare(JFileInfo info1, JFileInfo info2) {
return info1.relativePath.compareTo(info2.relativePath);
}
});
} catch (Exception e) {
Log.e(QtTAG, "Failed to list App's APK files with " + e.toString());
}
return fileInfos;
}
}

View File

@ -39,7 +39,7 @@ abstract class QtLoader {
private final Resources m_resources;
private final String m_packageName;
private String m_preferredAbi = null;
private String m_nativeLibrariesDir = null;
private String m_extractedNativeLibsDir = null;
private ClassLoader m_classLoader;
protected ComponentInfo m_contextInfo;
@ -253,7 +253,7 @@ abstract class QtLoader {
if (nativeLibraryDir.exists()) {
String[] list = nativeLibraryDir.list();
if (nativeLibraryDir.isDirectory() && list != null && list.length > 0) {
m_nativeLibrariesDir = nativeLibraryPrefix;
m_extractedNativeLibsDir = nativeLibraryPrefix;
}
}
} else {
@ -280,7 +280,7 @@ abstract class QtLoader {
String[] list = systemLibraryDir.list();
if (systemLibraryDir.exists()) {
if (systemLibraryDir.isDirectory() && list != null && list.length > 0)
m_nativeLibrariesDir = systemLibsPrefix;
m_extractedNativeLibsDir = systemLibsPrefix;
else
Log.e(QtTAG, "System library directory " + systemLibsPrefix + " is empty.");
} else {
@ -288,8 +288,8 @@ abstract class QtLoader {
}
}
if (m_nativeLibrariesDir != null && !m_nativeLibrariesDir.endsWith("/"))
m_nativeLibrariesDir += "/";
if (m_extractedNativeLibsDir != null && !m_extractedNativeLibsDir.endsWith("/"))
m_extractedNativeLibsDir += "/";
}
/**
@ -362,6 +362,23 @@ abstract class QtLoader {
return m_resources.getStringArray(id);
}
/**
* Returns true if the app is uses native libraries stored uncompressed in the APK.
**/
private static boolean isUncompressedNativeLibs()
{
int flags = QtNative.getContext().getApplicationInfo().flags;
return (flags & ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS) == 0;
}
/**
* Returns the native shared libraries path inside relative to the app's APK.
**/
private String getApkNativeLibrariesDir()
{
return QtApkFileEngine.getAppApkFilePath() + "!/lib/" + Build.SUPPORTED_ABIS[0] + "/";
}
/**
* Returns QtLoader.LoadingResult.Succeeded if libraries are successfully loaded,
* QtLoader.LoadingResult.AlreadyLoaded if they have already been loaded,
@ -376,17 +393,22 @@ abstract class QtLoader {
return LoadingResult.Failed;
}
if (m_nativeLibrariesDir == null)
if (m_extractedNativeLibsDir == null)
parseNativeLibrariesDir();
if (m_nativeLibrariesDir == null || m_nativeLibrariesDir.isEmpty()) {
Log.e(QtTAG, "The native libraries directory is null or empty");
return LoadingResult.Failed;
if (isUncompressedNativeLibs()) {
String apkLibPath = getApkNativeLibrariesDir();
setEnvironmentVariable("QT_PLUGIN_PATH", apkLibPath);
setEnvironmentVariable("QML_PLUGIN_PATH", apkLibPath);
} else {
if (m_extractedNativeLibsDir == null || m_extractedNativeLibsDir.isEmpty()) {
Log.e(QtTAG, "The native libraries directory is null or empty");
return LoadingResult.Failed;
}
setEnvironmentVariable("QT_PLUGIN_PATH", m_extractedNativeLibsDir);
setEnvironmentVariable("QML_PLUGIN_PATH", m_extractedNativeLibsDir);
}
setEnvironmentVariable("QT_PLUGIN_PATH", m_nativeLibrariesDir);
setEnvironmentVariable("QML_PLUGIN_PATH", m_nativeLibrariesDir);
// Load native Qt APK libraries
ArrayList<String> nativeLibraries = getQtLibrariesList();
nativeLibraries.addAll(getLocalLibrariesList());
@ -434,17 +456,23 @@ abstract class QtLoader {
}
// Loading libraries using System.load() uses full lib paths
// or System.loadLibrary() for uncompressed libs
@SuppressLint("UnsafeDynamicallyLoadedCode")
private String loadLibraryHelper(String library)
{
String loadedLib = null;
try {
File libFile = new File(library);
if (libFile.exists()) {
System.load(library);
loadedLib = library;
if (library.startsWith("/")) {
if (libFile.exists()) {
System.load(library);
loadedLib = library;
} else {
Log.e(QtTAG, "Can't find '" + library + "'");
}
} else {
Log.e(QtTAG, "Can't find '" + library + "'");
System.loadLibrary(library);
loadedLib = library;
}
} catch (Exception e) {
Log.e(QtTAG, "Can't load '" + library + "'", e);
@ -465,13 +493,16 @@ abstract class QtLoader {
for (String libName : libraries) {
// Add lib and .so to the lib name only if it doesn't already end with .so,
// this means some names don't necessarily need to have the lib prefix
if (!libName.endsWith(".so")) {
libName = libName + ".so";
libName = "lib" + libName;
if (isUncompressedNativeLibs()) {
if (libName.endsWith(".so"))
libName = libName.substring(3, libName.length() - 3);
absolutePathLibraries.add(libName);
} else {
if (!libName.endsWith(".so"))
libName = "lib" + libName + ".so";
File file = new File(m_extractedNativeLibsDir + libName);
absolutePathLibraries.add(file.getAbsolutePath());
}
File file = new File(m_nativeLibrariesDir + libName);
absolutePathLibraries.add(file.getAbsolutePath());
}
return absolutePathLibraries;
@ -492,6 +523,8 @@ abstract class QtLoader {
m_mainLibPath = loadLibraryHelper(mainLibPath);
if (m_mainLibPath == null)
success[0] = false;
else if (isUncompressedNativeLibs())
m_mainLibPath = getApkNativeLibrariesDir() + "lib" + m_mainLibPath + ".so";
});
return success[0];

View File

@ -41,9 +41,6 @@ android {
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'

View File

@ -37,6 +37,7 @@
// come with sandboxes that kill applications that make system calls outside a
// whitelist and several Android vendors can't be bothered to update the list.
# undef STATX_BASIC_STATS
#include <private/qjnihelpers_p.h>
#endif
QT_BEGIN_NAMESPACE
@ -433,6 +434,15 @@ static std::vector<MountInfo> parseMountInfo(FilterMountInfo filter = FilterMoun
void QStorageInfoPrivate::doStat()
{
#ifdef Q_OS_ANDROID
if (QtAndroidPrivate::isUncompressedNativeLibs()) {
// We need to pass the actual file path on the file system to statfs64
QString possibleApk = QtAndroidPrivate::resolveApkPath(rootPath);
if (!possibleApk.isEmpty())
rootPath = possibleApk;
}
#endif
retrieveVolumeInfo();
if (!ready)
return;

View File

@ -2409,6 +2409,10 @@ void QCoreApplicationPrivate::setApplicationFilePath(const QString &path)
the executable, which may be inside an application bundle (if the
application is bundled).
On Android this will point to the directory actually containing the
executable, which may be inside the application APK (if it was built
with uncompressed libraries support).
\warning On Linux, this function will try to get the path from the
\c {/proc} file system. If that fails, it assumes that \c
{argv[0]} contains the absolute file name of the executable. The

View File

@ -12,16 +12,23 @@
#include <QtCore/private/qcoreapplication_p.h>
#include <QtCore/private/qlocking_p.h>
#if QT_CONFIG(regularexpression)
#include <QtCore/qregularexpression.h>
#endif
#include <android/log.h>
#include <deque>
#include <memory>
QT_BEGIN_NAMESPACE
Q_DECLARE_JNI_CLASS(QtLoader, "org/qtproject/qt/android/QtLoader")
Q_DECLARE_JNI_CLASS(QtInputDelegate, "org/qtproject/qt/android/QtInputDelegate");
Q_DECLARE_JNI_CLASS(MotionEvent, "android/view/MotionEvent");
Q_DECLARE_JNI_CLASS(KeyEvent, "android/view/KeyEvent");
using namespace Qt::StringLiterals;
namespace QtAndroidPrivate {
// *Listener virtual function implementations.
// Defined out-of-line to pin the vtable/type_info.
@ -246,6 +253,28 @@ void QtAndroidPrivate::handleResume()
listeners.at(i)->handleResume();
}
bool QtAndroidPrivate::isUncompressedNativeLibs()
{
const static bool isUncompressed = QtJniTypes::QtLoader::callStaticMethod<bool>(
"isUncompressedNativeLibs");
return isUncompressed;
}
QString QtAndroidPrivate::resolveApkPath(const QString &fileName)
{
#if QT_CONFIG(regularexpression)
const static QRegularExpression inApkRegex("(.+\\.apk)!\\/.+"_L1);
auto match = inApkRegex.matchView(fileName);
if (match.hasMatch())
return match.captured(1);
#else
if (int index = fileName.lastIndexOf(u".apk!/"); index > 0)
return fileName.mid(0, index + 4);
#endif
return {};
}
jint QtAndroidPrivate::initJNI(JavaVM *vm, JNIEnv *env)
{
g_javaVM = vm;

View File

@ -106,6 +106,9 @@ namespace QtAndroidPrivate
Q_CORE_EXPORT bool acquireAndroidDeadlockProtector();
Q_CORE_EXPORT void releaseAndroidDeadlockProtector();
Q_CORE_EXPORT bool isUncompressedNativeLibs();
Q_CORE_EXPORT QString resolveApkPath(const QString &fileName);
}
#define Q_JNI_FIND_AND_CHECK_CLASS(CLASS_NAME) \

View File

@ -12,6 +12,7 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin
DEFAULT_IF "android" IN_LIST QT_QPA_PLATFORMS
SOURCES
androidcontentfileengine.cpp androidcontentfileengine.h
qandroidapkfileengine.h qandroidapkfileengine.cpp
androiddeadlockprotector.h
androidjniaccessibility.cpp androidjniaccessibility.h
androidjniinput.cpp androidjniinput.h

View File

@ -8,6 +8,7 @@
#include <semaphore.h>
#include "androidcontentfileengine.h"
#include "qandroidapkfileengine.h"
#include "androiddeadlockprotector.h"
#include "androidjniaccessibility.h"
#include "androidjniinput.h"
@ -83,6 +84,7 @@ static double m_density = 1.0;
static AndroidAssetsFileEngineHandler *m_androidAssetsFileEngineHandler = nullptr;
static AndroidContentFileEngineHandler *m_androidContentFileEngineHandler = nullptr;
static QAndroidApkFileEngineHandler *m_androidApkFileEngineHandler = nullptr;
static AndroidBackendRegister *m_backendRegister = nullptr;
@ -384,6 +386,7 @@ static jboolean startQtAndroidPlugin(JNIEnv *env, jobject /*object*/, jstring pa
m_androidPlatformIntegration = nullptr;
m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler();
m_androidContentFileEngineHandler = new AndroidContentFileEngineHandler();
m_androidApkFileEngineHandler = new QAndroidApkFileEngineHandler();
m_mainLibraryHnd = nullptr;
m_backendRegister = new AndroidBackendRegister();
@ -496,6 +499,8 @@ static void quitQtAndroidPlugin(JNIEnv *env, jclass /*clazz*/)
m_androidAssetsFileEngineHandler = nullptr;
delete m_androidContentFileEngineHandler;
m_androidContentFileEngineHandler = nullptr;
delete m_androidApkFileEngineHandler;
m_androidApkFileEngineHandler = nullptr;
}
static void terminateQt(JNIEnv *env, jclass /*clazz*/)
@ -533,6 +538,8 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/)
m_androidPlatformIntegration = nullptr;
delete m_androidAssetsFileEngineHandler;
m_androidAssetsFileEngineHandler = nullptr;
delete m_androidApkFileEngineHandler;
m_androidApkFileEngineHandler = nullptr;
delete m_backendRegister;
m_backendRegister = nullptr;
sem_post(&m_exitSemaphore);

View File

@ -0,0 +1,265 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qandroidapkfileengine.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QJniEnvironment>
#include <QtCore/QReadWriteLock>
#include <private/qjnihelpers_p.h>
QT_BEGIN_NAMESPACE
Q_DECLARE_JNI_CLASS(JFileInfo, "org/qtproject/qt/android/JFileInfo")
Q_DECLARE_JNI_CLASS(MappedByteBuffer, "java/nio/MappedByteBuffer")
using namespace Qt::StringLiterals;
using namespace QtJniTypes;
using namespace QNativeInterface;
typedef QList<QAndroidApkFileEngine::FileInfo> ApkFileInfos;
namespace {
struct ApkFileInfosGlobalData
{
QReadWriteLock apkInfosLock;
ApkFileInfos apkFileInfos;
};
}
Q_GLOBAL_STATIC(ApkFileInfosGlobalData, g_apkFileInfosGlobal)
static ApkFileInfos *apkFileInfos()
{
{
QReadLocker lock(&g_apkFileInfosGlobal->apkInfosLock);
if (!g_apkFileInfosGlobal->apkFileInfos.isEmpty())
return &g_apkFileInfosGlobal->apkFileInfos;
}
QWriteLocker lock(&g_apkFileInfosGlobal->apkInfosLock);
ArrayList arrayList = QtApkFileEngine::callStaticMethod<ArrayList>(
"getApkFileInfos", QAndroidApkFileEngine::apkPath());
for (int i = 0; i < arrayList.callMethod<int>("size"); ++i) {
JFileInfo jInfo = arrayList.callMethod<jobject>("get", i);
QAndroidApkFileEngine::FileInfo info;
info.relativePath = jInfo.getField<QString>("relativePath");
info.size = jInfo.getField<jlong>("size");
info.isDir = jInfo.getField<jboolean>("isDir");
g_apkFileInfosGlobal->apkFileInfos.append(info);
}
return &g_apkFileInfosGlobal->apkFileInfos;
}
QAndroidApkFileEngine::QAndroidApkFileEngine(const QString &fileName)
: m_apkFileEngine(QAndroidApplication::context())
{
setFileName(fileName);
QString relativePath = QAndroidApkFileEngine::relativePath(m_fileName);
for (QAndroidApkFileEngine::FileInfo &info : *apkFileInfos()) {
if (info.relativePath == relativePath) {
m_fileInfo = &info;
break;
}
}
}
QAndroidApkFileEngine::~QAndroidApkFileEngine()
{
close();
}
QString QAndroidApkFileEngine::apkPath()
{
static QString apkPath = QtApkFileEngine::callStaticMethod<QString>("getAppApkFilePath");
return apkPath;
}
QString QAndroidApkFileEngine::relativePath(const QString &filePath)
{
const static int apkPathPrefixSize = apkPath().size() + 2;
return filePath.right(filePath.size() - apkPathPrefixSize);
}
bool QAndroidApkFileEngine::open(QIODevice::OpenMode openMode,
std::optional<QFile::Permissions> permissions)
{
Q_UNUSED(permissions);
if (!(openMode & QIODevice::ReadOnly))
return false;
if (!m_apkFileEngine.isValid() || !m_fileInfo)
return false;
if (m_fileInfo->relativePath.isEmpty())
return false;
return m_apkFileEngine.callMethod<bool>("open", m_fileInfo->relativePath);
}
bool QAndroidApkFileEngine::close()
{
return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod<bool>("close") : false;
}
qint64 QAndroidApkFileEngine::size() const
{
return m_fileInfo ? m_fileInfo->size : -1;
}
qint64 QAndroidApkFileEngine::pos() const
{
return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod<jlong>("pos") : -1;
}
bool QAndroidApkFileEngine::seek(qint64 pos)
{
return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod<bool>("seek", jint(pos)) : false;
}
qint64 QAndroidApkFileEngine::read(char *data, qint64 maxlen)
{
if (!m_apkFileEngine.isValid())
return -1;
QJniArray<jbyte> byteArray = m_apkFileEngine.callMethod<jbyte[]>("read", jlong(maxlen));
QJniEnvironment env;
env->GetByteArrayRegion(byteArray.arrayObject(), 0, byteArray.size(), (jbyte*)data);
if (env.checkAndClearExceptions())
return -1;
return byteArray.size();
}
QAbstractFileEngine::FileFlags QAndroidApkFileEngine::fileFlags(FileFlags type) const
{
if (m_fileInfo) {
FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag);
if (m_fileInfo->isDir)
return type & (DirectoryType | commonFlags);
else
return type & (FileType | commonFlags);
}
return {};
}
QString QAndroidApkFileEngine::fileName(FileName file) const
{
switch (file) {
case PathName:
case AbsolutePathName:
case CanonicalPathName:
case DefaultName:
case AbsoluteName:
case CanonicalName:
return m_fileName;
case BaseName:
return m_fileName.mid(m_fileName.lastIndexOf(u'/') + 1);
default:
return QString();
}
}
void QAndroidApkFileEngine::setFileName(const QString &file)
{
m_fileName = file;
if (m_fileName.endsWith(u'/'))
m_fileName.chop(1);
}
uchar *QAndroidApkFileEngine::map(qint64 offset, qint64 size, QFileDevice::MemoryMapFlags flags)
{
if (flags & QFile::MapPrivateOption) {
qCritical() << "Mapping an in-APK file with private mode is not supported.";
return nullptr;
}
if (!m_apkFileEngine.isValid())
return nullptr;
const MappedByteBuffer mappedBuffer = m_apkFileEngine.callMethod<MappedByteBuffer>(
"getMappedByteBuffer", jlong(offset), jlong(size));
if (!mappedBuffer.isValid())
return nullptr;
void *address = QJniEnvironment::getJniEnv()->GetDirectBufferAddress(mappedBuffer.object());
return const_cast<uchar *>(reinterpret_cast<const uchar *>(address));
}
bool QAndroidApkFileEngine::extension(Extension extension, const ExtensionOption *option,
ExtensionReturn *output)
{
if (extension == MapExtension) {
const auto *options = static_cast<const MapExtensionOption *>(option);
auto *returnValue = static_cast<MapExtensionReturn *>(output);
returnValue->address = map(options->offset, options->size, options->flags);
return (returnValue->address != nullptr);
}
return false;
}
bool QAndroidApkFileEngine::supportsExtension(Extension extension) const
{
if (extension == MapExtension)
return true;
return false;
}
#ifndef QT_NO_FILESYSTEMITERATOR
QAbstractFileEngine::IteratorUniquePtr QAndroidApkFileEngine::beginEntryList(
const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
{
return std::make_unique<QAndroidApkFileEngineIterator>(path, filters, filterNames);
}
QAndroidApkFileEngineIterator::QAndroidApkFileEngineIterator(
const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
: QAbstractFileEngineIterator(path, filters, filterNames)
{
const QString relativePath = QAndroidApkFileEngine::relativePath(path);
for (QAndroidApkFileEngine::FileInfo &info : *apkFileInfos()) {
if (info.relativePath.startsWith(relativePath))
m_infos.append(&info);
}
}
QAndroidApkFileEngineIterator::~QAndroidApkFileEngineIterator() { }
bool QAndroidApkFileEngineIterator::advance()
{
if (!m_infos.isEmpty() && m_index < m_infos.size() - 1) {
++m_index;
return true;
}
return false;
}
QString QAndroidApkFileEngineIterator::currentFileName() const
{
return m_infos.at(m_index)->relativePath;
}
QString QAndroidApkFileEngineIterator::currentFilePath() const
{
return QAndroidApkFileEngine::apkPath() + "!/" + currentFileName();
}
#endif
std::unique_ptr<QAbstractFileEngine>
QAndroidApkFileEngineHandler::create(const QString &fileName) const
{
if (QtAndroidPrivate::resolveApkPath(fileName).isEmpty())
return {};
return std::make_unique<QAndroidApkFileEngine>(fileName);
}
QT_END_NAMESPACE

View File

@ -0,0 +1,88 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QANDROIDAPKFILEENGINE_H
#define QANDROIDAPKFILEENGINE_H
#include <QtCore/private/qabstractfileengine_p.h>
#include <QtCore/qjnitypes.h>
#include <QtCore/QJniObject>
QT_BEGIN_NAMESPACE
Q_DECLARE_JNI_CLASS(QtApkFileEngine, "org/qtproject/qt/android/QtApkFileEngine")
class QAndroidApkFileEngine : public QAbstractFileEngine
{
public:
QAndroidApkFileEngine(const QString &fileName);
~QAndroidApkFileEngine();
struct FileInfo
{
QString relativePath;
qint64 size = -1;
bool isDir;
};
bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override;
bool close() override;
qint64 size() const override;
qint64 pos() const override;
bool seek(qint64 pos) override;
qint64 read(char *data, qint64 maxlen) override;
FileFlags fileFlags(FileFlags type = FileInfoAll) const override;
bool caseSensitive() const override { return true; }
QString fileName(FileName file = DefaultName) const override;
void setFileName(const QString &file) override;
uchar *map(qint64 offset, qint64 size, QFile::MemoryMapFlags flags);
bool extension(Extension extension, const ExtensionOption *option = nullptr,
ExtensionReturn *output = nullptr) override;
bool supportsExtension(Extension extension) const override;
static QString apkPath();
static QString relativePath(const QString &filePath);
#ifndef QT_NO_FILESYSTEMITERATOR
IteratorUniquePtr beginEntryList(const QString &, QDirListing::IteratorFlags filters,
const QStringList &filterNames) override;
#endif
private:
QString m_fileName;
FileInfo *m_fileInfo = nullptr;
QtJniTypes::QtApkFileEngine m_apkFileEngine;
};
#ifndef QT_NO_FILESYSTEMITERATOR
class QAndroidApkFileEngineIterator : public QAbstractFileEngineIterator
{
public:
QAndroidApkFileEngineIterator(const QString &path,
QDirListing::IteratorFlags filters,
const QStringList &filterNames);
~QAndroidApkFileEngineIterator();
bool advance() override;
QString currentFileName() const override;
QString currentFilePath() const override;
private:
int m_index = -1;
QList<QAndroidApkFileEngine::FileInfo *> m_infos;
};
#endif
class QAndroidApkFileEngineHandler : public QAbstractFileEngineHandler
{
Q_DISABLE_COPY_MOVE(QAndroidApkFileEngineHandler)
public:
QAndroidApkFileEngineHandler() = default;
std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override;
};
QT_END_NAMESPACE
#endif // QANDROIDAPKFILEENGINE_H

View File

@ -764,7 +764,6 @@ bool copyFileIfNewer(const QString &sourceFileName,
struct GradleBuildConfigs {
QString appNamespace;
bool setsLegacyPackaging = false;
bool usesIntegerCompileSdkVersion = false;
};
@ -797,9 +796,7 @@ GradleBuildConfigs gradleBuildConfigs(const QString &path)
const QByteArray trimmedLine = line.trimmed();
if (isComment(trimmedLine))
continue;
if (trimmedLine.contains("useLegacyPackaging")) {
configs.setsLegacyPackaging = true;
} else if (trimmedLine.contains("compileSdkVersion androidCompileSdkVersion.toInteger()")) {
if (trimmedLine.contains("compileSdkVersion androidCompileSdkVersion.toInteger()")) {
configs.usesIntegerCompileSdkVersion = true;
} else if (trimmedLine.contains("namespace")) {
configs.appNamespace = QString::fromUtf8(extractValue(trimmedLine));
@ -2972,8 +2969,6 @@ bool buildAndroidProject(const Options &options)
const QString gradleBuildFilePath = options.outputDirectory + "build.gradle"_L1;
GradleBuildConfigs gradleConfigs = gradleBuildConfigs(gradleBuildFilePath);
if (!gradleConfigs.setsLegacyPackaging)
gradleProperties["android.bundle.enableUncompressedNativeLibs"] = "false";
gradleProperties["buildDir"] = "build";
gradleProperties["qtAndroidDir"] =

View File

@ -5,7 +5,7 @@
#include <QtCore/qlibraryinfo.h>
#include <QtCore/qscopeguard.h>
#include <QtCore/private/qlibraryinfo_p.h>
#include <QStandardPaths>
class tst_QLibraryInfo : public QObject
{
@ -87,7 +87,7 @@ void tst_QLibraryInfo::merge()
QLibraryInfoPrivate::setQtconfManualPath(&qtConfPath);
QLibraryInfoPrivate::reload();
QString baseDir = QCoreApplication::applicationDirPath();
QString baseDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QString docPath = QLibraryInfo::path(QLibraryInfo::DocumentationPath);
// we can't know where exactly the doc path points, but it should not point to ${baseDir}/doc,
// which would be the behavior without merge_qt_conf

View File

@ -29,6 +29,10 @@
#include <QtCore/private/qfunctions_win_p.h>
#endif
#ifdef Q_OS_ANDROID
#include <QtCore/private/qjnihelpers_p.h>
#endif
#include <QtTest/private/qemulationdetector_p.h>
#ifdef Q_OS_WIN
@ -1387,6 +1391,10 @@ void tst_QFile::permissions_data()
#ifndef Q_OS_WASM
// Application path is empty on wasm
#ifdef Q_OS_ANDROID
// Android in-APK application path doesn't report exec permission
if (!QtAndroidPrivate::isUncompressedNativeLibs())
#endif
QTest::newRow("data0") << QCoreApplication::instance()->applicationFilePath() << uint(QFile::ExeUser) << true << false;
#endif
QTest::newRow("data1") << m_testSourceFile << uint(QFile::ReadUser) << true << false;
@ -2715,7 +2723,12 @@ void tst_QFile::virtualFile()
lines += std::move(data);
}
if (!QT_CONFIG(static) && !QTestPrivate::isRunningArmOnX86()) {
if (!QT_CONFIG(static) && !QTestPrivate::isRunningArmOnX86()
#ifdef Q_OS_ANDROID
// With uncompressed libs, only the app's APK path is shown and no library names.
&& !QtAndroidPrivate::isUncompressedNativeLibs()
#endif
) {
// we must be able to find QtCore and QtTest somewhere
static const char corelib[] = "libQt" QT_STRINGIFY(QT_VERSION_MAJOR) "Core";
static const char testlib[] = "libQt" QT_STRINGIFY(QT_VERSION_MAJOR) "Test";

View File

@ -17,6 +17,8 @@ list(APPEND test_data "tst_qstandardpaths.cpp")
qt_internal_add_test(tst_qstandardpaths
SOURCES
tst_qstandardpaths.cpp
LIBRARIES
Qt::CorePrivate
TESTDATA ${test_data}
)

View File

@ -13,6 +13,9 @@
#if defined(Q_OS_WIN)
# include <qt_windows.h>
#endif
#ifdef Q_OS_ANDROID
#include <private/qjnihelpers_p.h>
#endif
#ifdef Q_OS_UNIX
#include <unistd.h>
@ -529,10 +532,15 @@ void tst_qstandardpaths::testFindExecutableLinkToDirectory()
#ifdef Q_OS_WASM
QSKIP("No applicationdir on wasm");
#else
const QString appPath = QCoreApplication::applicationDirPath();
#ifdef Q_OS_ANDROID
if (QtAndroidPrivate::isUncompressedNativeLibs())
QSKIP("Can't create a link to applicationDir which points inside an APK on Android");
#endif
// link to directory
const QString target = QDir::tempPath() + QDir::separator() + QLatin1String("link.lnk");
QFile::remove(target);
QFile appFile(QCoreApplication::applicationDirPath());
QFile appFile(appPath);
QVERIFY(appFile.link(target));
QVERIFY(QStandardPaths::findExecutable(target).isEmpty());
QFile::remove(target);

View File

@ -5,6 +5,7 @@ if(ANDROID)
add_subdirectory(android)
add_subdirectory(android_appless)
add_subdirectory(androiditemmodel)
add_subdirectory(android_legacy_packaging)
endif()
if(WIN32)
add_subdirectory(windows)

View File

@ -0,0 +1,19 @@
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_android_legacy_packaging LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
endif()
qt_internal_add_test(tst_android_legacy_packaging
SOURCES
tst_android_legacy_packaging.cpp
)
if(ANDROID)
set_property(TARGET tst_android_legacy_packaging PROPERTY
QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/testdata
)
endif()

View File

@ -0,0 +1,84 @@
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.6.0'
}
}
repositories {
google()
mavenCentral()
}
apply plugin: qtGradlePluginType
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.core:core:1.13.1'
}
android {
/*******************************************************
* The following variables:
* - androidBuildToolsVersion,
* - androidCompileSdkVersion
* - qtAndroidDir - holds the path to qt android files
* needed to build any Qt application
* on Android.
* - qtGradlePluginType - whether to build an app or a library
*
* are defined in gradle.properties file. This file is
* updated by QtCreator and androiddeployqt tools.
* Changing them manually might break the compilation!
*******************************************************/
namespace androidPackageName
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
ndkVersion androidNdkVersion
// Extract native libraries from the APK
packagingOptions.jniLibs.useLegacyPackaging true
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = [qtAndroidDir + '/src', 'src', 'java']
aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl']
res.srcDirs = [qtAndroidDir + '/res', 'res']
resources.srcDirs = ['resources']
renderscript.srcDirs = ['src']
assets.srcDirs = ['assets']
jniLibs.srcDirs = ['libs']
}
}
tasks.withType(JavaCompile) {
options.incremental = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
lintOptions {
abortOnError false
}
// Do not compress Qt binary resources file
aaptOptions {
noCompress 'rcc'
}
defaultConfig {
resConfig "en"
minSdkVersion qtMinSdkVersion
targetSdkVersion qtTargetSdkVersion
ndk.abiFilters = qtTargetAbiList.split(",")
}
}

View File

@ -0,0 +1,20 @@
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
using namespace Qt::StringLiterals;
class tst_AndroidLegacyPackaging : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
};
void tst_AndroidLegacyPackaging::initTestCase()
{ }
QTEST_MAIN(tst_AndroidLegacyPackaging)
#include "tst_android_legacy_packaging.moc"

View File

@ -11,6 +11,10 @@
#include <private/qplugin_p.h>
#ifdef Q_OS_ANDROID
#include <private/qjnihelpers_p.h>
#endif
class tst_QPlugin : public QObject
{
Q_OBJECT
@ -198,6 +202,11 @@ void tst_QPlugin::scanInvalidPlugin()
{
QFile f(newName);
#ifdef Q_OS_ANDROID
// set write permission to plugin file that's copied from read-only location
if (QtAndroidPrivate::isUncompressedNativeLibs())
f.setPermissions(QFileDevice::WriteOwner | f.permissions());
#endif
QVERIFY(f.open(QIODevice::ReadWrite | QIODevice::Unbuffered));
QVERIFY(f.size() > qint64(strlen(invalidPluginSignature)));
uchar *data = f.map(0, f.size());

View File

@ -18,6 +18,10 @@
# include <QtCore/private/qmachparser_p.h>
#endif
#ifdef Q_OS_ANDROID
#include <private/qjnihelpers_p.h>
#endif
using namespace Qt::StringLiterals;
// Helper macros to let us know if some suffixes are valid
@ -724,6 +728,12 @@ static void loadCorruptElf_helper(const QString &origLibrary)
QFETCH(QString, snippet);
QFETCH(ElfPatcher, patcher);
#ifdef Q_OS_ANDROID
// patchElf() tries to map with private mode
if (QtAndroidPrivate::isUncompressedNativeLibs())
QSKIP("Mapping in-APK libraries with private mode is not supported on Android");
#endif
std::unique_ptr<QTemporaryFile> tmplib = patchElf(origLibrary, patcher);
QPluginLoader lib(tmplib->fileName());

View File

@ -12,6 +12,10 @@
#include <qcoreapplication.h>
#include <quuid.h>
#ifdef Q_OS_ANDROID
#include <QStandardPaths>
#endif
class tst_QUuid : public QObject
{
Q_OBJECT
@ -82,7 +86,7 @@ void tst_QUuid::initTestCase()
#if QT_CONFIG(process)
// chdir to the directory containing our testdata, then refer to it with relative paths
#ifdef Q_OS_ANDROID
QString testdata_dir = QCoreApplication::applicationDirPath();
QString testdata_dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
#else // !Q_OS_ANDROID
QString testdata_dir = QFileInfo(QFINDTESTDATA("testProcessUniqueness")).absolutePath();
#endif