Skip to content

Instantly share code, notes, and snippets.

@sh-zam
Created September 2, 2022 07:28
Show Gist options
  • Save sh-zam/3d1683c3c7fbae32de62c9ca07f5dd7a to your computer and use it in GitHub Desktop.
Save sh-zam/3d1683c3c7fbae32de62c9ca07f5dd7a to your computer and use it in GitHub Desktop.
**
** Copyright (C) 2019 Volker Krause <vkrause@kde.org>
** 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 "androidcontentfileengine.h"
#include <private/qjni_p.h>
#include <private/qjnihelpers_p.h>
#include <QDebug>
AndroidContentFileEngine::AndroidContentFileEngine(const QString &f)
: m_fd(-1)
, m_file(f)
, m_resolvedName(QString())
, m_safFileManager(QJNIObjectPrivate::callStaticObjectMethod(
"org/qtproject/qt5/android/SAFFileManager", "instance",
"()Lorg/qtproject/qt5/android/SAFFileManager;"))
, m_safError(m_safFileManager.getObjectField("mError",
"Lorg/qtproject/qt5/android/FileError;"))
{
m_resolvedName = getResolvedFileName(f);
setFileName(f);
}
bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode)
{
QString openModeStr;
ProcessOpenModeResult res = processOpenModeFlags(openMode);
if (!res.ok) {
setError(QFileDevice::OpenError, res.error);
return false;
}
// if Truncate flag is set we have to set 'r' as well, else we get inconsistent results.
if ((res.openMode & QFileDevice::ReadOnly) || (res.openMode & QFileDevice::Truncate)) {
openModeStr += QLatin1Char('r');
}
if (res.openMode & QFileDevice::WriteOnly) {
openModeStr += QLatin1Char('w');
}
if (res.openMode & QFileDevice::Truncate) {
openModeStr += QLatin1Char('t');
} else if (res.openMode & QFileDevice::Append) {
qWarning("Android doesn't support 'a' mode when accessing a ContentProvider");
openModeStr += QLatin1Char('a');
}
const auto fd = m_safFileManager.callMethod<jint>(
"openFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I",
QJNIObjectPrivate::fromString(m_file).object(),
QJNIObjectPrivate::fromString(openModeStr).object());
if (fd < 0) {
setErrorFromSAF();
return false;
}
setFileDescriptor(fd);
return QFSFileEngine::open(openMode, m_fd, QFile::DontCloseHandle);
}
bool AndroidContentFileEngine::close()
{
setErrorFromSAF();
return m_safFileManager.callMethod<jboolean>("closeFileDescriptor", "(I)Z", m_fd);
}
QJNIObjectPrivate toJavaUri(const QString &stringUri)
{
const auto uri = QJNIObjectPrivate::callStaticObjectMethod(
"android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;",
QJNIObjectPrivate::fromString(stringUri).object());
if (!uri.isValid()) {
qWarning("Invalid Uri returned");
}
return uri;
}
bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories) const
{
return m_safFileManager.callMethod<jboolean>(
"mkdir", "(Ljava/lang/String;Z)Z",
QJNIObjectPrivate::fromString(dirName).object(), createParentDirectories);
}
bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const
{
if (recurseParentDirectories) {
qWarning() << "rmpath(): Unsupported";
}
return m_safFileManager.callMethod<jboolean>(
"delete", "(Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(dirName).object());
}
qint64 AndroidContentFileEngine::size() const
{
const jlong size = m_safFileManager.callMethod<jlong>(
"getSize", "(Ljava/lang/String;)J",
QJNIObjectPrivate::fromString(m_file).object());
return (qint64)size;
}
AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const
{
const FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag);
FileFlags flags;
const bool exists =
m_safFileManager.callMethod<jboolean>("exists", "(Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(m_file).object());
if (!exists)
return flags;
flags = commonFlags;
const bool canWrite =
m_safFileManager.callMethod<jboolean>("canWrite", "(Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(m_file).object());
if (canWrite) {
flags |= (WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm);
}
const bool isDir = m_safFileManager.callMethod<jboolean>(
"isDir", "(Ljava/lang/String;)Z", QJNIObjectPrivate::fromString(m_file).object());
if (isDir) {
flags = DirectoryType | flags;
} else {
flags = FileType | flags;
}
return type & flags;
}
QString AndroidContentFileEngine::fileName(FileName f) const
{
switch (f) {
case DefaultName: {
// the file isn't created here, so the resolved filename is empty
if (m_resolvedName.isEmpty()) {
const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/')));
return m_file.mid(pos + 1);
} else {
return m_resolvedName;
}
}
case PathName:
case AbsoluteName:
case AbsolutePathName:
case CanonicalName:
return m_file;
case CanonicalPathName: {
const bool isTree = m_safFileManager.callMethod<jboolean>(
"isTreeUri", "(Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(m_file).object());
if (isTree) {
const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/')));
return m_file.left(pos);
}
return m_file;
}
case BaseName: {
const int pos = m_resolvedName.lastIndexOf(QChar(QLatin1Char('/')));
return m_resolvedName.mid(pos);
}
default:
return QString();
}
}
bool AndroidContentFileEngine::isRelativePath() const
{
if (m_file.startsWith(QLatin1String("content://"))) {
return false;
} else {
// well then it's just a Unix path
return m_file.length() ? m_file.at(0) != QLatin1Char('/') : true;
}
}
bool AndroidContentFileEngine::rename(const QString &newName)
{
auto renameSaf = [this](QString newName) -> bool {
return m_safFileManager.callMethod<jboolean>(
"rename", "(Ljava/lang/String;Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(m_file).object(),
QJNIObjectPrivate::fromString(newName).object());
};
// if the file doesn't have scheme it means the newName is only the fileName part
if (!newName.startsWith("content://")) {
return renameSaf(newName);
}
auto getPos = [](QString file) {
int posDecoded = file.lastIndexOf(QLatin1String("/"));
int posEncoded = file.lastIndexOf(QLatin1String("%2F"));
return posEncoded > posDecoded ? posEncoded : posDecoded;
};
const QString parent = m_file.left(getPos(m_file));
if (newName.contains(parent)) {
const int pos = getPos(newName);
const QString displayName = newName.mid(pos + 1);
return renameSaf(displayName);
}
m_resolvedName = getResolvedFileName(m_file);
return false;
}
bool AndroidContentFileEngine::remove()
{
return m_safFileManager.callMethod<jboolean>(
"delete", "(Ljava/lang/String;)Z",
QJNIObjectPrivate::fromString(m_file).object());
}
QString AndroidContentFileEngine::getResolvedFileName(const QString &path) const
{
QJNIObjectPrivate resolvedName = m_safFileManager.callObjectMethod(
"getFileName", "(Ljava/lang/String;)Ljava/lang/String;",
QJNIObjectPrivate::fromString(path).object());
if (resolvedName.isValid()) {
return resolvedName.toString();
}
return QString();
}
QAbstractFileEngine::Iterator *
AndroidContentFileEngine::beginEntryList(QDir::Filters filters,
const QStringList &filterNames)
{
return new AndroidContentFileEngineIterator(m_safFileManager, filters, filterNames);
}
QAbstractFileEngine::Iterator *AndroidContentFileEngine::endEntryList()
{
return nullptr;
}
void AndroidContentFileEngine::setFileDescriptor(const int fd) { m_fd = fd; }
void AndroidContentFileEngine::setErrorFromSAF()
{
auto error = m_safError.callMethod<jint>("getError");
auto errorString =
m_safError.callObjectMethod("getErrorString", "()Ljava/lang/String;");
if (errorString.isValid()) {
setError(static_cast<QFileDevice::FileError>(error), errorString.toString());
}
}
AndroidContentFileEngineHandler::AndroidContentFileEngineHandler() = default;
AndroidContentFileEngineHandler::~AndroidContentFileEngineHandler() = default;
QAbstractFileEngine *
AndroidContentFileEngineHandler::create(const QString &fileName) const
{
if (!fileName.startsWith(QLatin1String("content"))) {
return nullptr;
}
return new AndroidContentFileEngine(fileName);
}
AndroidContentFileEngineIterator::AndroidContentFileEngineIterator(
QJNIObjectPrivate safFileManager, QDir::Filters filters,
const QStringList &filterNames)
: QAbstractFileEngineIterator(filters, filterNames)
, m_safFileManager(safFileManager)
{
}
AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator()
{
m_safFileManager.callMethod<void>("resetListCache");
}
QString AndroidContentFileEngineIterator::next()
{
if (!hasNext()) {
return QString();
}
m_index++;
// just like it is in QFSFileEngineIterator
return currentFilePath();
}
bool AndroidContentFileEngineIterator::hasNext() const
{
if (m_index == -2) {
fetchEntries();
m_index++;
}
return m_index < m_entries.size() && m_entries.size() > 0;
}
QString AndroidContentFileEngineIterator::currentFileName() const
{
if (!hasNext()) {
return QString();
}
return m_entries.at(m_index);
}
void AndroidContentFileEngineIterator::fetchEntries() const
{
QJNIObjectPrivate fileNames = m_safFileManager.callObjectMethod(
"listFileNames", "(Ljava/lang/String;)[Ljava/lang/String;",
QJNIObjectPrivate::fromString(path()).object());
if (!fileNames.isValid()) {
return;
}
QJNIEnvironmentPrivate env;
const jsize length = env->GetArrayLength(static_cast<jarray>(fileNames.object()));
for (int i = 0; i < length; ++i) {
QJNIObjectPrivate elem(
env->GetObjectArrayElement(static_cast<jobjectArray>(fileNames.object()), i));
m_entries << elem.toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment