/**************************************************************************** ** ** Copyright (C) 2012 BogDan Vatra ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** 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 Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qandroidassetsfileenginehandler.h" #include "androidjnimain.h" #include #include #include #include QT_BEGIN_NAMESPACE static const QLatin1String assetsPrefix("assets:"); const static int prefixSize = 7; static inline QString cleanedAssetPath(QString file) { if (file.startsWith(assetsPrefix)) file.remove(0, prefixSize); file.replace(QLatin1String("//"), QLatin1String("/")); if (file.startsWith(QLatin1Char('/'))) file.remove(0, 1); if (file.endsWith(QLatin1Char('/'))) file.chop(1); return file; } static inline QString prefixedPath(QString path) { path = assetsPrefix + QLatin1Char('/') + path; path.replace(QLatin1String("//"), QLatin1String("/")); return path; } struct AssetItem { enum class Type { File, Folder, Invalid }; AssetItem() = default; AssetItem (const QString &rawName) : name(rawName) { if (name.endsWith(QLatin1Char('/'))) { type = Type::Folder; name.chop(1); } } Type type = Type::File; QString name; }; using AssetItemList = QVector; class FolderIterator : public AssetItemList { public: static QSharedPointer fromCache(const QString &path, bool clone) { QMutexLocker lock(&m_assetsCacheMutex); QSharedPointer *folder = m_assetsCache.object(path); if (!folder) { folder = new QSharedPointer{new FolderIterator{path}}; if ((*folder)->empty() || !m_assetsCache.insert(path, folder)) { QSharedPointer res = *folder; delete folder; return res; } } return clone ? QSharedPointer{new FolderIterator{*(*folder)}} : *folder; } static AssetItem::Type fileType(const QString &filePath) { const QStringList paths = filePath.split(QLatin1Char('/')); QString fullPath; AssetItem::Type res = AssetItem::Type::Invalid; for (const auto &path: paths) { auto folder = fromCache(fullPath, false); auto it = std::lower_bound(folder->begin(), folder->end(), AssetItem{path}, [](const AssetItem &val, const AssetItem &assetItem) { return val.name < assetItem.name; }); if (it == folder->end() || it->name != path) return AssetItem::Type::Invalid; if (!fullPath.isEmpty()) fullPath.append(QLatin1Char('/')); fullPath += path; res = it->type; } return res; } FolderIterator(const FolderIterator &other) : AssetItemList(other) , m_index(-1) , m_path(other.m_path) {} FolderIterator(const QString &path) : m_path(path) { QJNIObjectPrivate files = QJNIObjectPrivate::callStaticObjectMethod(QtAndroid::applicationClass(), "listAssetContent", "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;", QtAndroid::assets(), QJNIObjectPrivate::fromString(path).object()); if (files.isValid()) { QJNIEnvironmentPrivate env; jobjectArray jFiles = static_cast(files.object()); const jint nFiles = env->GetArrayLength(jFiles); for (int i = 0; i < nFiles; ++i) { AssetItem item{QJNIObjectPrivate(env->GetObjectArrayElement(jFiles, i)).toString()}; insert(std::upper_bound(begin(), end(), item, [](const auto &a, const auto &b){ return a.name < b.name; }), item); } } m_path = assetsPrefix + QLatin1Char('/') + m_path + QLatin1Char('/'); m_path.replace(QLatin1String("//"), QLatin1String("/")); } QString currentFileName() const { if (m_index < 0 || m_index >= size()) return {}; return at(m_index).name; } QString currentFilePath() const { if (m_index < 0 || m_index >= size()) return {}; return m_path + at(m_index).name; } bool hasNext() const { return !empty() && m_index + 1 < size(); } std::optional> next() { if (!hasNext()) return {}; ++m_index; return std::pair(currentFileName(), at(m_index)); } private: int m_index = -1; QString m_path; static QCache> m_assetsCache; static QMutex m_assetsCacheMutex; }; QCache> FolderIterator::m_assetsCache(std::max(50, qEnvironmentVariableIntValue("QT_ANDROID_MAX_ASSETS_CACHE_SIZE"))); QMutex FolderIterator::m_assetsCacheMutex; class AndroidAbstractFileEngineIterator: public QAbstractFileEngineIterator { public: AndroidAbstractFileEngineIterator(QDir::Filters filters, const QStringList &nameFilters, const QString &path) : QAbstractFileEngineIterator(filters, nameFilters) { m_stack.push_back(FolderIterator::fromCache(cleanedAssetPath(path), true)); if (m_stack.last()->empty()) m_stack.pop_back(); } QFileInfo currentFileInfo() const override { return QFileInfo(currentFilePath()); } QString currentFileName() const override { if (!m_currentIterator) return {}; return m_currentIterator->currentFileName(); } virtual QString currentFilePath() const { if (!m_currentIterator) return {}; return m_currentIterator->currentFilePath(); } bool hasNext() const override { if (m_stack.empty()) return false; if (!m_stack.last()->hasNext()) { m_stack.pop_back(); return hasNext(); } return true; } QString next() override { if (m_stack.empty()) { m_currentIterator.reset(); return {}; } m_currentIterator = m_stack.last(); auto res = m_currentIterator->next(); if (!res) return {}; if (res->second.type == AssetItem::Type::Folder) { m_stack.push_back(FolderIterator::fromCache(cleanedAssetPath(currentFilePath()), true)); if (m_stack.last()->empty()) m_stack.pop_back(); } return res->first; } private: mutable QSharedPointer m_currentIterator; mutable QVector> m_stack; }; class AndroidAbstractFileEngine: public QAbstractFileEngine { public: explicit AndroidAbstractFileEngine(AAssetManager *assetManager, const QString &fileName) : m_assetManager(assetManager) { setFileName(fileName); } ~AndroidAbstractFileEngine() { close(); } bool open(QIODevice::OpenMode openMode) override { if (m_isFolder || (openMode & QIODevice::WriteOnly)) return false; close(); m_assetFile = AAssetManager_open(m_assetManager, m_fileName.toUtf8(), AASSET_MODE_BUFFER); return m_assetFile; } bool close() override { if (m_assetFile) { AAsset_close(m_assetFile); m_assetFile = 0; return true; } m_isFolder = false; return false; } qint64 size() const override { if (m_assetFile) return AAsset_getLength(m_assetFile); return -1; } qint64 pos() const override { if (m_assetFile) return AAsset_seek(m_assetFile, 0, SEEK_CUR); return -1; } bool seek(qint64 pos) override { if (m_assetFile) return pos == AAsset_seek(m_assetFile, pos, SEEK_SET); return false; } qint64 read(char *data, qint64 maxlen) override { if (m_assetFile) return AAsset_read(m_assetFile, data, maxlen); return -1; } bool isSequential() const override { return false; } bool caseSensitive() const override { return true; } bool isRelativePath() const override { return false; } FileFlags fileFlags(FileFlags type = FileInfoAll) const override { FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); FileFlags flags; if (m_assetFile) flags = FileType | commonFlags; else if (m_isFolder) flags = DirectoryType | commonFlags; return type & flags; } QString fileName(FileName file = DefaultName) const override { int pos; switch (file) { case DefaultName: case AbsoluteName: case CanonicalName: return prefixedPath(m_fileName); case BaseName: if ((pos = m_fileName.lastIndexOf(QChar(QLatin1Char('/')))) != -1) return prefixedPath(m_fileName.mid(pos)); else return prefixedPath(m_fileName); case PathName: case AbsolutePathName: case CanonicalPathName: if ((pos = m_fileName.lastIndexOf(QChar(QLatin1Char('/')))) != -1) return prefixedPath(m_fileName.left(pos)); else return prefixedPath(m_fileName); default: return QString(); } } void setFileName(const QString &file) override { if (m_fileName == cleanedAssetPath(file)) return; close(); m_fileName = cleanedAssetPath(file); switch (FolderIterator::fileType(m_fileName)) { case AssetItem::Type::File: open(QIODevice::ReadOnly); break; case AssetItem::Type::Folder: m_isFolder = true; break; case AssetItem::Type::Invalid: break; } } Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override { if (m_isFolder) return new AndroidAbstractFileEngineIterator(filters, filterNames, m_fileName); return nullptr; } private: AAsset *m_assetFile = nullptr; AAssetManager *m_assetManager = nullptr; QString m_fileName; bool m_isFolder = false; }; AndroidAssetsFileEngineHandler::AndroidAssetsFileEngineHandler() { m_assetManager = QtAndroid::assetManager(); } QAbstractFileEngine * AndroidAssetsFileEngineHandler::create(const QString &fileName) const { if (fileName.isEmpty()) return nullptr; if (!fileName.startsWith(assetsPrefix)) return nullptr; QString path = fileName.mid(prefixSize); path.replace(QLatin1String("//"), QLatin1String("/")); if (path.startsWith(QLatin1Char('/'))) path.remove(0, 1); if (path.endsWith(QLatin1Char('/'))) path.chop(1); return new AndroidAbstractFileEngine(m_assetManager, path); } QT_END_NAMESPACE