diff --git a/src/android/jar/CMakeLists.txt b/src/android/jar/CMakeLists.txt index 79e63af8343..1b15d6bc6ee 100644 --- a/src/android/jar/CMakeLists.txt +++ b/src/android/jar/CMakeLists.txt @@ -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 diff --git a/src/android/jar/src/org/qtproject/qt/android/QtApkFileEngine.java b/src/android/jar/src/org/qtproject/qt/android/QtApkFileEngine.java new file mode 100644 index 00000000000..356f48454a2 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/QtApkFileEngine.java @@ -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 getApkFileInfos(String apkPath) + { + ArrayList fileInfos = new ArrayList<>(); + HashSet dirSet = new HashSet<>(); + HashSet allDirsSet = new HashSet<>(); + + try (ZipFile zipFile = new ZipFile(apkPath)) { + Enumeration 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() { + @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; + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/QtLoader.java b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java index b9eff5d8ac7..1c1159b792c 100644 --- a/src/android/jar/src/org/qtproject/qt/android/QtLoader.java +++ b/src/android/jar/src/org/qtproject/qt/android/QtLoader.java @@ -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 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]; diff --git a/src/android/templates/build.gradle b/src/android/templates/build.gradle index 72f1fdb750a..c44b35843cd 100644 --- a/src/android/templates/build.gradle +++ b/src/android/templates/build.gradle @@ -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' diff --git a/src/corelib/io/qstorageinfo_linux.cpp b/src/corelib/io/qstorageinfo_linux.cpp index 9cd9adb176b..26a62ffb8cf 100644 --- a/src/corelib/io/qstorageinfo_linux.cpp +++ b/src/corelib/io/qstorageinfo_linux.cpp @@ -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 #endif QT_BEGIN_NAMESPACE @@ -433,6 +434,15 @@ static std::vector 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; diff --git a/src/corelib/kernel/qcoreapplication.cpp b/src/corelib/kernel/qcoreapplication.cpp index cdc4b47db90..45189115ab4 100644 --- a/src/corelib/kernel/qcoreapplication.cpp +++ b/src/corelib/kernel/qcoreapplication.cpp @@ -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 diff --git a/src/corelib/kernel/qjnihelpers.cpp b/src/corelib/kernel/qjnihelpers.cpp index 4d5ccd7d9b1..d477070c310 100644 --- a/src/corelib/kernel/qjnihelpers.cpp +++ b/src/corelib/kernel/qjnihelpers.cpp @@ -12,16 +12,23 @@ #include #include +#if QT_CONFIG(regularexpression) +#include +#endif + #include #include #include 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( + "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; diff --git a/src/corelib/kernel/qjnihelpers_p.h b/src/corelib/kernel/qjnihelpers_p.h index f0c0a416831..8ccb1d398bc 100644 --- a/src/corelib/kernel/qjnihelpers_p.h +++ b/src/corelib/kernel/qjnihelpers_p.h @@ -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) \ diff --git a/src/plugins/platforms/android/CMakeLists.txt b/src/plugins/platforms/android/CMakeLists.txt index aaf62dd7e71..6bdeff079b5 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -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 diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index bd4b18e057e..07e74e10779 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -8,6 +8,7 @@ #include #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); diff --git a/src/plugins/platforms/android/qandroidapkfileengine.cpp b/src/plugins/platforms/android/qandroidapkfileengine.cpp new file mode 100644 index 00000000000..fcc24333d92 --- /dev/null +++ b/src/plugins/platforms/android/qandroidapkfileengine.cpp @@ -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 +#include +#include + +#include + +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 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( + "getApkFileInfos", QAndroidApkFileEngine::apkPath()); + + for (int i = 0; i < arrayList.callMethod("size"); ++i) { + JFileInfo jInfo = arrayList.callMethod("get", i); + QAndroidApkFileEngine::FileInfo info; + info.relativePath = jInfo.getField("relativePath"); + info.size = jInfo.getField("size"); + info.isDir = jInfo.getField("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("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 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("open", m_fileInfo->relativePath); +} + +bool QAndroidApkFileEngine::close() +{ + return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod("close") : false; +} + +qint64 QAndroidApkFileEngine::size() const +{ + return m_fileInfo ? m_fileInfo->size : -1; +} + +qint64 QAndroidApkFileEngine::pos() const +{ + return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod("pos") : -1; +} + +bool QAndroidApkFileEngine::seek(qint64 pos) +{ + return m_apkFileEngine.isValid() ? m_apkFileEngine.callMethod("seek", jint(pos)) : false; +} + +qint64 QAndroidApkFileEngine::read(char *data, qint64 maxlen) +{ + if (!m_apkFileEngine.isValid()) + return -1; + + QJniArray byteArray = m_apkFileEngine.callMethod("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( + "getMappedByteBuffer", jlong(offset), jlong(size)); + + if (!mappedBuffer.isValid()) + return nullptr; + + void *address = QJniEnvironment::getJniEnv()->GetDirectBufferAddress(mappedBuffer.object()); + + return const_cast(reinterpret_cast(address)); +} + +bool QAndroidApkFileEngine::extension(Extension extension, const ExtensionOption *option, + ExtensionReturn *output) +{ + if (extension == MapExtension) { + const auto *options = static_cast(option); + auto *returnValue = static_cast(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(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 +QAndroidApkFileEngineHandler::create(const QString &fileName) const +{ + if (QtAndroidPrivate::resolveApkPath(fileName).isEmpty()) + return {}; + + return std::make_unique(fileName); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidapkfileengine.h b/src/plugins/platforms/android/qandroidapkfileengine.h new file mode 100644 index 00000000000..c6d928746cc --- /dev/null +++ b/src/plugins/platforms/android/qandroidapkfileengine.h @@ -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 +#include +#include + +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 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 m_infos; +}; +#endif + +class QAndroidApkFileEngineHandler : public QAbstractFileEngineHandler +{ + Q_DISABLE_COPY_MOVE(QAndroidApkFileEngineHandler) +public: + QAndroidApkFileEngineHandler() = default; + + std::unique_ptr create(const QString &fileName) const override; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDAPKFILEENGINE_H diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index cf13c5e781a..c3ca363507a 100644 --- a/src/tools/androiddeployqt/main.cpp +++ b/src/tools/androiddeployqt/main.cpp @@ -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"] = diff --git a/tests/auto/corelib/global/qlibraryinfo/tst_qlibraryinfo.cpp b/tests/auto/corelib/global/qlibraryinfo/tst_qlibraryinfo.cpp index b7d79c05f57..1d5315d6266 100644 --- a/tests/auto/corelib/global/qlibraryinfo/tst_qlibraryinfo.cpp +++ b/tests/auto/corelib/global/qlibraryinfo/tst_qlibraryinfo.cpp @@ -5,7 +5,7 @@ #include #include #include - +#include 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 diff --git a/tests/auto/corelib/io/qfile/tst_qfile.cpp b/tests/auto/corelib/io/qfile/tst_qfile.cpp index 969de5ae198..f9138dfcfab 100644 --- a/tests/auto/corelib/io/qfile/tst_qfile.cpp +++ b/tests/auto/corelib/io/qfile/tst_qfile.cpp @@ -29,6 +29,10 @@ #include #endif +#ifdef Q_OS_ANDROID +#include +#endif + #include #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"; diff --git a/tests/auto/corelib/io/qstandardpaths/CMakeLists.txt b/tests/auto/corelib/io/qstandardpaths/CMakeLists.txt index 90bc0f3b70c..03dfa2a94ff 100644 --- a/tests/auto/corelib/io/qstandardpaths/CMakeLists.txt +++ b/tests/auto/corelib/io/qstandardpaths/CMakeLists.txt @@ -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} ) diff --git a/tests/auto/corelib/io/qstandardpaths/tst_qstandardpaths.cpp b/tests/auto/corelib/io/qstandardpaths/tst_qstandardpaths.cpp index 84d0a505a4e..cff764be8be 100644 --- a/tests/auto/corelib/io/qstandardpaths/tst_qstandardpaths.cpp +++ b/tests/auto/corelib/io/qstandardpaths/tst_qstandardpaths.cpp @@ -13,6 +13,9 @@ #if defined(Q_OS_WIN) # include #endif +#ifdef Q_OS_ANDROID +#include +#endif #ifdef Q_OS_UNIX #include @@ -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); diff --git a/tests/auto/corelib/platform/CMakeLists.txt b/tests/auto/corelib/platform/CMakeLists.txt index 51bc13e56e9..3a66ec2eae6 100644 --- a/tests/auto/corelib/platform/CMakeLists.txt +++ b/tests/auto/corelib/platform/CMakeLists.txt @@ -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) diff --git a/tests/auto/corelib/platform/android_legacy_packaging/CMakeLists.txt b/tests/auto/corelib/platform/android_legacy_packaging/CMakeLists.txt new file mode 100644 index 00000000000..997daae4821 --- /dev/null +++ b/tests/auto/corelib/platform/android_legacy_packaging/CMakeLists.txt @@ -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() diff --git a/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle b/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle new file mode 100644 index 00000000000..72f1fdb750a --- /dev/null +++ b/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle @@ -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(",") + } +} diff --git a/tests/auto/corelib/platform/android_legacy_packaging/tst_android_legacy_packaging.cpp b/tests/auto/corelib/platform/android_legacy_packaging/tst_android_legacy_packaging.cpp new file mode 100644 index 00000000000..4b066c38321 --- /dev/null +++ b/tests/auto/corelib/platform/android_legacy_packaging/tst_android_legacy_packaging.cpp @@ -0,0 +1,20 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include + +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" diff --git a/tests/auto/corelib/plugin/qplugin/tst_qplugin.cpp b/tests/auto/corelib/plugin/qplugin/tst_qplugin.cpp index 9443ba9e5c1..5aa1b15a4ea 100644 --- a/tests/auto/corelib/plugin/qplugin/tst_qplugin.cpp +++ b/tests/auto/corelib/plugin/qplugin/tst_qplugin.cpp @@ -11,6 +11,10 @@ #include +#ifdef Q_OS_ANDROID +#include +#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()); diff --git a/tests/auto/corelib/plugin/qpluginloader/tst_qpluginloader.cpp b/tests/auto/corelib/plugin/qpluginloader/tst_qpluginloader.cpp index 54da124f87b..c5a5bb40b8e 100644 --- a/tests/auto/corelib/plugin/qpluginloader/tst_qpluginloader.cpp +++ b/tests/auto/corelib/plugin/qpluginloader/tst_qpluginloader.cpp @@ -18,6 +18,10 @@ # include #endif +#ifdef Q_OS_ANDROID +#include +#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 tmplib = patchElf(origLibrary, patcher); QPluginLoader lib(tmplib->fileName()); diff --git a/tests/auto/corelib/plugin/quuid/tst_quuid.cpp b/tests/auto/corelib/plugin/quuid/tst_quuid.cpp index 3527764f931..8ce8f52064c 100644 --- a/tests/auto/corelib/plugin/quuid/tst_quuid.cpp +++ b/tests/auto/corelib/plugin/quuid/tst_quuid.cpp @@ -12,6 +12,10 @@ #include #include +#ifdef Q_OS_ANDROID +#include +#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